Compare commits
7 Commits
9e5141bcee
...
master
Author | SHA1 | Date | |
---|---|---|---|
bd2ddd9bb6 | |||
358dc77e5a | |||
1108a66378 | |||
bff627f4cf | |||
9bbdf19897 | |||
723863b971 | |||
af49f49bbf |
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**' # matches every branch
|
- "**" # matches every branch
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_and_check:
|
lint_and_check:
|
||||||
@@ -15,14 +15,17 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'npm'
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
|
|
||||||
- name: Run check
|
- name: Run check
|
||||||
run: npm run check
|
run: pnpm run check
|
||||||
|
44
EVENT_DATES_GUIDE.md
Normal file
44
EVENT_DATES_GUIDE.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Event Dates Management Guide
|
||||||
|
|
||||||
|
## How to Block Sign-ups on Event Days
|
||||||
|
|
||||||
|
The sign-up form automatically closes at 3pm on specified event dates to prevent last-minute registrations.
|
||||||
|
|
||||||
|
### Managing Event Dates
|
||||||
|
|
||||||
|
1. Open the `event-dates.json` file in the project root
|
||||||
|
2. Add or remove dates in the `eventDates` array
|
||||||
|
3. Use the format `YYYY-MM-DD` (e.g., "2024-12-25" for December 25, 2024)
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventDates": ["2025-09-05", "1999-01-01"],
|
||||||
|
"cutoffTime": "15:00",
|
||||||
|
"message": "Sign-ups are closed for today's event. Please come back tomorrow."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
- The cutoff time is set to 3pm (15:00) by default
|
||||||
|
- Sign-ups will automatically reopen at midnight after an event day
|
||||||
|
- Users will see a friendly message when sign-ups are closed
|
||||||
|
- The time zone follows the server's local time
|
||||||
|
|
||||||
|
### Adding New Event Dates
|
||||||
|
|
||||||
|
Simply add a new date to the array:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"eventDates": [
|
||||||
|
"2024-12-25",
|
||||||
|
"2024-12-31",
|
||||||
|
"2025-01-15",
|
||||||
|
"2025-02-14",
|
||||||
|
"2025-03-20" // <- New date added
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to save the file after making changes!
|
@@ -5,15 +5,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CalendarIcon } from "lucide-react";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -22,11 +14,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { signupFormSubmit } from "@/lib/actions";
|
import { signupFormSubmit } from "@/lib/actions";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { isSignupBlocked } from "@/lib/signup-time-check";
|
||||||
|
|
||||||
export const signupFormSchema = z.object({
|
export const signupFormSchema = z.object({
|
||||||
name: z.string().min(2, { message: "Name is required" }).max(50, { message: "Name is too long" }),
|
name: z.string().min(2, { error: "Name is required" }).max(50, { error: "Name is too long" }),
|
||||||
email: z.string().email({ message: "Email is invalid" }),
|
email: z.email({ error: "Email is invalid" }),
|
||||||
dob: z.date({ required_error: "Birthday is required" }),
|
dob: z.date({ error: "Birthday is required" }),
|
||||||
});
|
});
|
||||||
export const youngestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 20));
|
export const youngestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 20));
|
||||||
export const oldestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 100));
|
export const oldestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 100));
|
||||||
@@ -34,6 +27,8 @@ export const oldestDate = new Date(new Date().setFullYear(new Date().getFullYear
|
|||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const [response, setResponse] = useState<string | null>(null);
|
const [response, setResponse] = useState<string | null>(null);
|
||||||
|
const signupStatus = isSignupBlocked();
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof signupFormSchema>>({
|
const form = useForm<z.infer<typeof signupFormSchema>>({
|
||||||
resolver: zodResolver(signupFormSchema),
|
resolver: zodResolver(signupFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -43,6 +38,12 @@ export default function SignUp() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
async function onSubmit(values: z.infer<typeof signupFormSchema>) {
|
async function onSubmit(values: z.infer<typeof signupFormSchema>) {
|
||||||
|
// Double-check signup isn't blocked before submitting
|
||||||
|
const currentStatus = isSignupBlocked();
|
||||||
|
if (currentStatus.blocked) {
|
||||||
|
setResponse(currentStatus.message || "Sign-ups are currently closed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
setResponse(await signupFormSubmit(values));
|
setResponse(await signupFormSubmit(values));
|
||||||
}
|
}
|
||||||
@@ -59,9 +60,7 @@ export default function SignUp() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="name@example.com" {...field} />
|
<Input placeholder="name@example.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>We will contact you here with information about events.</FormDescription>
|
||||||
We will contact you here with information about events.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -91,10 +90,7 @@ export default function SignUp() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className={cn(
|
className={cn("w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
||||||
"w-[240px] pl-3 text-left font-normal",
|
|
||||||
!field.value && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
|
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
@@ -122,12 +118,22 @@ export default function SignUp() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={submitted}>
|
<Button type="submit" disabled={submitted || signupStatus.blocked}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// If signup is blocked, show the message
|
||||||
|
if (signupStatus.blocked && !response) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-orange-50 p-6 text-center">
|
||||||
|
<p className="text-lg font-semibold text-orange-900 mb-2">Sign-ups Temporarily Closed</p>
|
||||||
|
<p className="text-orange-800">{signupStatus.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response ?? SignupForm();
|
return response ?? SignupForm();
|
||||||
}
|
}
|
||||||
|
@@ -2,23 +2,17 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
<input
|
||||||
({ className, type, ...props }, ref) => {
|
type={type}
|
||||||
return (
|
data-slot="input"
|
||||||
<input
|
className={cn(
|
||||||
type={type}
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className={cn(
|
className
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
ref={ref}
|
);
|
||||||
{...props}
|
}
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Input.displayName = "Input";
|
|
||||||
|
|
||||||
export { Input };
|
export { Input };
|
||||||
|
7
event-dates.json
Normal file
7
event-dates.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"eventDates": ["2025-09-05", "1999-01-01"],
|
||||||
|
"cutoffTime": "15:00",
|
||||||
|
"message": "Sign-ups are closed for today's event. Please come back tomorrow.",
|
||||||
|
|
||||||
|
"internalComment": "Add event dates in YYYY-MM-DD format. Signups will be disabled after 3pm (15:00) on these dates by default."
|
||||||
|
}
|
@@ -29,7 +29,7 @@ async function listmonk(data: listmonkData): Promise<string> {
|
|||||||
return "An error occurred or this email is already subscribed.";
|
return "An error occurred or this email is already subscribed.";
|
||||||
}
|
}
|
||||||
return "Thanks for signing up! Please check your email for a confirmation.";
|
return "Thanks for signing up! Please check your email for a confirmation.";
|
||||||
} catch (error) {
|
} catch {
|
||||||
return "An error occurred while trying to sign up. Please try again.";
|
return "An error occurred while trying to sign up. Please try again.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
lib/signup-time-check.ts
Normal file
23
lib/signup-time-check.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import eventConfig from "@/event-dates.json";
|
||||||
|
|
||||||
|
export function isSignupBlocked(): { blocked: boolean; message?: string } {
|
||||||
|
const now = new Date();
|
||||||
|
const currentDate = now.toISOString().split("T")[0]; // YYYY-MM-DD format
|
||||||
|
const currentTime = now.toTimeString().slice(0, 5); // HH:MM format
|
||||||
|
|
||||||
|
// Check if today is an event date
|
||||||
|
const isEventDay = eventConfig.eventDates.includes(currentDate);
|
||||||
|
|
||||||
|
if (isEventDay) {
|
||||||
|
// Check if current time is after the cutoff time (default 15:00 / 3pm)
|
||||||
|
const cutoffTime = eventConfig.cutoffTime || "15:00";
|
||||||
|
if (currentTime >= cutoffTime) {
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
message: eventConfig.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
9466
package-lock.json
generated
9466
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -9,27 +9,27 @@
|
|||||||
"check": "next lint && npx tsc --noEmit"
|
"check": "next lint && npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.0",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cssnano": "^7.0.1",
|
"cssnano": "^7.1.0",
|
||||||
"date-fns": "^4.0.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "^15.2.1",
|
"next": "^15.4.1",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.4",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-day-picker": "^9.5.1",
|
"react-day-picker": "^9.8.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.60.0",
|
||||||
"tailwind-merge": "^3.0.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^4.0.0"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.11",
|
"@tailwindcss/postcss": "4.1.11",
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"eslint": "9.31.0",
|
"eslint": "9.31.0",
|
||||||
"eslint-config-next": "15.3.5",
|
"eslint-config-next": "15.4.1",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"turbo": "2.5.4",
|
"turbo": "2.5.4",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@11.4.2"
|
"packageManager": "pnpm@10.13.1"
|
||||||
}
|
}
|
||||||
|
6286
pnpm-lock.yaml
generated
Normal file
6286
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@tailwindcss/oxide'
|
||||||
|
- sharp
|
@@ -22,6 +22,6 @@
|
|||||||
},
|
},
|
||||||
"target": "ES2023"
|
"target": "ES2023"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "event-dates.json"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user