diff --git a/app/components/CheckboxInput.tsx b/app/components/CheckboxInput.tsx new file mode 100644 index 0000000..e374157 --- /dev/null +++ b/app/components/CheckboxInput.tsx @@ -0,0 +1,81 @@ +"use client" +import styled from "styled-components" +import { forwardRef } from "react" + +// Types // + +type BaseCheckboxProps = Omit, "size" | "type"> + +interface CheckboxInputProps extends BaseCheckboxProps { + variant?: "primary" | "secondary" + size?: "small" | "default" +} + +// Components // + +export const CheckboxInput = forwardRef( + ({ variant = "secondary", size = "default", ...props }, ref) => { + return + } +) + +CheckboxInput.displayName = "CheckboxInput" + +// Styled Components // + +const StyledCheckbox = styled.input.attrs({ type: "checkbox" })<{ + $variant: "primary" | "secondary" + $size: "small" | "default" +}>` + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: ${(props) => (props.$size === "small" ? "1rem" : "1.25rem")}; + height: ${(props) => (props.$size === "small" ? "1rem" : "1.25rem")}; + border: 2px solid + ${(props) => + props.$variant === "secondary" ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"}; + border-radius: 0.25rem; + background-color: transparent; + cursor: pointer; + position: relative; + transition: all 0.2s ease; + margin: 0; + flex-shrink: 0; + + &:hover { + border-color: ${(props) => + props.$variant === "secondary" ? "rgba(255, 255, 255, 0.5)" : "rgba(0, 0, 0, 0.5)"}; + } + + &:checked { + border-color: ${(props) => + props.$variant === "secondary" ? "rgba(156, 163, 255, 0.9)" : "rgba(0, 0, 0, 0.7)"}; + background-color: ${(props) => + props.$variant === "secondary" ? "rgba(156, 163, 255, 0.9)" : "rgba(0, 0, 0, 0.7)"}; + } + + &:checked::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(45deg); + width: ${(props) => (props.$size === "small" ? "0.25rem" : "0.375rem")}; + height: ${(props) => (props.$size === "small" ? "0.5rem" : "0.625rem")}; + border: solid white; + border-width: 0 2px 2px 0; + } + + &:focus { + outline: 2px solid + ${(props) => + props.$variant === "secondary" ? "rgba(156, 163, 255, 0.5)" : "rgba(0, 0, 0, 0.3)"}; + outline-offset: 2px; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +` diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx new file mode 100644 index 0000000..e9ee1c8 --- /dev/null +++ b/app/components/Modal.tsx @@ -0,0 +1,95 @@ +"use client" +import { useEffect } from "react" +import { createPortal } from "react-dom" +import styled from "styled-components" + +// Types // + +interface ModalProps { + isOpen: boolean + onClose: () => void + children: React.ReactNode +} + +// Components // + +export const Modal = ({ isOpen, onClose, children }: ModalProps) => { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose() + } + } + + if (isOpen) { + document.addEventListener("keydown", handleEscape) + document.body.style.overflow = "hidden" + } + + return () => { + document.removeEventListener("keydown", handleEscape) + document.body.style.overflow = "unset" + } + }, [isOpen, onClose]) + + if (!isOpen) return null + + if (typeof document === "undefined") return null + + return createPortal( + + e.stopPropagation()}> + + ✕ + + {children} + + , + document.body + ) +} + +// Styled Components // + +const Backdrop = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +` + +const ModalContent = styled.div` + background-color: rgba(20, 20, 20, 0.95); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + padding: 2rem; + max-width: 500px; + width: 100%; + position: relative; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +` + +const CloseButton = styled.button` + position: absolute; + top: 1rem; + right: 1rem; + background: transparent; + border: none; + color: white; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem; + line-height: 1; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.7; + } +` diff --git a/app/doorbell/page.tsx b/app/doorbell/page.tsx index a0789c7..fe44099 100644 --- a/app/doorbell/page.tsx +++ b/app/doorbell/page.tsx @@ -3,9 +3,11 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react" import type { RealtimeChannel } from "@supabase/supabase-js" import styled from "styled-components" import { motion } from "framer-motion" +import Link from "next/link" import { PotionBackground } from "../components/PotionBackground" import { ErrorBoundary } from "../components/ErrorBoundary" import { Button } from "../components/Button" +import { Modal } from "../components/Modal" import { supabaseClient } from "@/lib/supabaseClient" import eventsData from "../data/events.json" import type { LumaEvent } from "../services/luma" @@ -15,6 +17,7 @@ import type { LumaEvent } from "../services/luma" export default function Doorbell() { const [isRinging, setIsRinging] = useState(false) const [ringCount, setRingCount] = useState(0) + const [isModalOpen, setIsModalOpen] = useState(false) const channelRef = useRef(null) const lastRingIdRef = useRef(null) @@ -66,6 +69,13 @@ export default function Doorbell() { setRingCount((prev) => prev + 1) } + const handleAgree = () => { + if (nearestEvent?.url) { + window.open(nearestEvent.url, "_blank") + } + setIsModalOpen(false) + } + useEffect(() => { const client = supabaseClient if (!client) { @@ -122,14 +132,8 @@ export default function Doorbell() { {nearestEvent && ( - )} @@ -168,6 +172,24 @@ export default function Doorbell() { )} + setIsModalOpen(false)}> + Event Terms + + By checking in I agree to the{" "} + + Event Terms + + . + + + + + + ) } @@ -288,6 +310,40 @@ const CallMessage = styled.p` color: white; ` +const ModalTitle = styled.h2` + font-size: 1.75rem; + font-weight: 700; + margin: 0 0 1.5rem 0; + color: white; + text-align: center; +` + +const ModalText = styled.p` + font-size: 1rem; + line-height: 1.6; + margin: 0 0 2rem 0; + color: rgba(255, 255, 255, 0.9); + text-align: center; + font-style: italic; +` + +const TermsLink = styled(Link)` + color: white; + text-decoration: underline; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.7; + } +` + +const ModalButtons = styled.div` + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +` + // Constants // type RingPayload = { diff --git a/app/event-terms/page.tsx b/app/event-terms/page.tsx new file mode 100644 index 0000000..76d387e --- /dev/null +++ b/app/event-terms/page.tsx @@ -0,0 +1,258 @@ +"use client" +import styled from "styled-components" +import { PotionBackground } from "@/app/components/PotionBackground" +import { ErrorBoundary } from "@/app/components/ErrorBoundary" + +export default function EventTerms() { + return ( + <> + + } + > + + + +
+ +
+ DEVx Event Terms and Conditions + + Thank you for your interest in attending a DEVx event. Before completing your + registration, please read these terms to understand the nature of the event and + expectations for all participants. We look forward to seeing you at our next meetup. + +
+ +
+ Nature of Event + + DEVx events are community-organized meetups hosted by volunteer organizers. These + events are designed to foster a fun and educational environment for developers of all + skill levels. DEVx organizers arrange venues, coordinate presentations, and facilitate + networking opportunities, but do not control all aspects of the venue or guarantee + specific outcomes from attendance. + +
+ +
+ General Acknowledgments + By registering for this event, you acknowledge and agree that: + + You are 18+ years old or have parental/guardian consent to attend + You voluntarily assume all risks associated with attendance + You have read and understood these terms + This is a community-organized event run by volunteers + The event may be cancelled or modified without notice + + You are responsible for any costs incurred (travel, accommodation, etc.) related to + event changes + + +
+ +
+ Photography and Recording + + Events may be photographed and/or recorded. These materials may capture your name, + voice, image, or likeness. By attending, you grant permission for your image to be + used in photos, videos, and other media without compensation. If you prefer not to be + photographed, please request an opt-out indicator at check-in. + +
+ +
+ Code of Conduct + + Professional, respectful conduct is expected from all attendees. Harassment, + discrimination, or disruptive behavior will result in removal from the event. You + agree to comply with all venue rules and event organizer instructions. + +
+ +
+ Venue Terms + + The event venue may have additional terms, conditions, and rules that apply to your + attendance. By attending, you agree to comply with all venue requirements. + +
+ +
+ Minors + + Individuals under 18 years of age must be accompanied by a parent or legal guardian at + all times during the event. Attendees may be required to provide proof of age upon + request. + +
+ +
+ Assumption of Risk and Release + + You assume all risks and accept sole responsibility for any injury (including, but not + limited to, personal injury, disability, and death), illness, damage, loss, claim, + liability, or expense, of any kind, that you may experience or incur in connection + with attending the event. + + + You hereby release, covenant not to sue, discharge, and hold harmless DEVx, its + organizers, volunteers, representatives, and the venue owner, of and from any such + claims, including all liabilities, claims, actions, damages, costs, or expenses of any + kind arising out of or relating thereto. + +
+ +
+ Health Considerations + + You acknowledge the risk of exposure to communicable diseases, including COVID-19, and + voluntarily assume the risk of exposure or infection by attending the event, and that + such exposure or infection may result in personal injury, illness, disability, and/or + death. You understand that the risk of becoming exposed to or infected at the event + may result from the actions, omissions, or negligence of others who may attend the + event. + + + Accordingly, you understand and agree that this release includes any claims based on + the actions, omissions, or negligence of DEVx, its organizers, volunteers, and + representatives, whether an infection or injury occurs before, during, or after + participation in the event. + + + You agree to comply with all health and safety procedures that may be implemented by + the event organizer or venue, in order to protect the health and safety of all event + attendees. + +
+ +
+ Data Collection + + By registering, you provide certain personal information for event coordination + purposes. You may optionally consent to receive additional communications. Your data + will be processed in the United States. + +
+ +
+ Governing Law and Disputes + + These terms are governed by the laws of the State of California. Any dispute arising + out of or relating to your attendance at the event shall be resolved in the state or + federal courts located in San Diego County, California. You and DEVx agree to submit + to the personal jurisdiction of such courts. You agree to bring any dispute in your + individual capacity, and not as a plaintiff or class member in any purported class, + collective, or representative proceeding. + +
+
+
+ + ) +} + +const BackgroundContainer = styled.section` + background-color: #0a0a0a; + position: fixed; + height: 100vh; + width: 100vw; + top: 0; + left: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` + +const Main = styled.main` + position: relative; + z-index: 1; + color: white; +` + +const TermsSection = styled.section` + max-width: 900px; + margin: 0 auto; + padding: 8rem 2rem 4rem; + + @media (max-width: 768px) { + padding: 6rem 1.5rem 3rem; + } + + @media (max-width: 480px) { + padding: 5rem 1rem 2rem; + } +` + +const Title = styled.h1` + font-size: clamp(1.75rem, 5vw, 2.5rem); + font-weight: 700; + margin-bottom: 1.5rem; + text-align: center; + color: white; +` + +const IntroText = styled.p` + font-size: 1.125rem; + line-height: 1.7; + text-align: center; + margin-bottom: 3rem; + color: rgba(255, 255, 255, 0.85); + + @media (max-width: 768px) { + font-size: 1rem; + margin-bottom: 2.5rem; + } +` + +const Section = styled.section` + margin-bottom: 2.5rem; +` + +const SectionTitle = styled.h2` + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: white; + + @media (max-width: 768px) { + font-size: 1.375rem; + } +` + +const Paragraph = styled.p` + font-size: 1.0625rem; + line-height: 1.8; + margin-bottom: 1rem; + color: rgba(255, 255, 255, 0.8); + + &:last-child { + margin-bottom: 0; + } + + @media (max-width: 768px) { + font-size: 1rem; + line-height: 1.7; + } +` + +const List = styled.ul` + margin: 1rem 0 0 1.5rem; + color: rgba(255, 255, 255, 0.8); +` + +const ListItem = styled.li` + font-size: 1.0625rem; + line-height: 1.8; + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + + @media (max-width: 768px) { + font-size: 1rem; + line-height: 1.7; + } +` diff --git a/app/submit-talk/page.tsx b/app/submit-talk/page.tsx index 3849fac..62bf528 100644 --- a/app/submit-talk/page.tsx +++ b/app/submit-talk/page.tsx @@ -9,6 +9,7 @@ import { Button } from "../components/Button" import { TextInput } from "../components/TextInput" import { TextareaInput } from "../components/TextareaInput" import { RadioInput } from "../components/RadioInput" +import { CheckboxInput } from "../components/CheckboxInput" import { PageContainer } from "../components/PageContainer" import { SuccessMessage as SuccessMessageComponent } from "../components/SuccessMessage" import Link from "next/link" @@ -26,6 +27,7 @@ export default function SubmitTalk() { const [profileId, setProfileId] = useState(null) const [profilePhoneNumber, setProfilePhoneNumber] = useState(null) const [isEditingPhone, setIsEditingPhone] = useState(false) + const [agreedToTerms, setAgreedToTerms] = useState(false) const [formData, setFormData] = useState({ phoneNumber: "", talkTitle: "", @@ -202,6 +204,12 @@ export default function SubmitTalk() { } } + // Terms agreement validation + if (!agreedToTerms) { + setError("You must agree to the terms to submit") + return + } + setSubmitting(true) setError(null) @@ -260,6 +268,7 @@ export default function SubmitTalk() { slidesUrl: "", slidesFile: null }) + setAgreedToTerms(false) setIsEditingPhone(false) // Scroll to top to show success message @@ -473,6 +482,23 @@ export default function SubmitTalk() { )} + + setAgreedToTerms(e.target.checked)} + required + /> + + By submitting, I agree to these{" "} + + terms + + + +