Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions app/components/CheckboxInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client"
import styled from "styled-components"
import { forwardRef } from "react"

// Types //

type BaseCheckboxProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "type">

interface CheckboxInputProps extends BaseCheckboxProps {
variant?: "primary" | "secondary"
size?: "small" | "default"
}

// Components //

export const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(
({ variant = "secondary", size = "default", ...props }, ref) => {
return <StyledCheckbox ref={ref} $variant={variant} $size={size} {...props} />
}
)

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;
}
`
95 changes: 95 additions & 0 deletions app/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Backdrop onClick={onClose}>
<ModalContent onClick={(e) => e.stopPropagation()}>
<CloseButton onClick={onClose} aria-label="Close modal">
</CloseButton>
{children}
</ModalContent>
</Backdrop>,
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;
}
`
72 changes: 64 additions & 8 deletions app/doorbell/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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<RealtimeChannel | null>(null)
const lastRingIdRef = useRef<string | null>(null)

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -122,14 +132,8 @@ export default function Doorbell() {
</ParagraphSection>
{nearestEvent && (
<ButtonSection>
<Button
href={nearestEvent.url}
variant="primary"
size="default"
target="_blank"
rel="noopener noreferrer"
>
Get Your Ticket
<Button variant="primary" size="default" onClick={() => setIsModalOpen(true)}>
Check In
</Button>
</ButtonSection>
)}
Expand Down Expand Up @@ -168,6 +172,24 @@ export default function Doorbell() {
)}
</Hero>
</Main>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalTitle>Event Terms</ModalTitle>
<ModalText>
By checking in I agree to the{" "}
<TermsLink href="/event-terms" target="_blank" rel="noopener noreferrer">
Event Terms
</TermsLink>
.
</ModalText>
<ModalButtons>
<Button variant="secondary" size="default" onClick={() => setIsModalOpen(false)}>
Decline
</Button>
<Button variant="primary" size="default" onClick={handleAgree}>
I Agree
</Button>
</ModalButtons>
</Modal>
</>
)
}
Expand Down Expand Up @@ -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 = {
Expand Down
Loading