Compare commits

..

1 Commits

Author SHA1 Message Date
9685f9188e chore(deps): update actions/checkout action to v5
Some checks failed
Check / Lint and Check (push) Failing after 3s
Check / Lint and Check (pull_request) Failing after 2s
2025-08-16 01:02:45 +00:00
15 changed files with 1257 additions and 1641 deletions

6
.eslintrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View File

@@ -13,19 +13,19 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run check
run: pnpm run lint
run: pnpm run check

View File

@@ -41,7 +41,7 @@ export default function SignUp() {
// Double-check signup isn't blocked before submitting
const currentStatus = isSignupBlocked();
if (currentStatus.blocked) {
setResponse(currentStatus.message ?? "Sign-ups are currently closed.");
setResponse(currentStatus.message || "Sign-ups are currently closed.");
return;
}
setSubmitted(true);
@@ -50,7 +50,6 @@ export default function SignUp() {
function SignupForm() {
return (
<Form {...form}>
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
@@ -91,10 +90,8 @@ export default function SignUp() {
<FormControl>
<Button
variant={"outline"}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
className={cn("w-[240px] pl-3 text-left font-normal", field.value && "text-muted-foreground")}
className={cn("w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>

View File

@@ -3,19 +3,26 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
interface FormFieldContextValue<
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> {
> = {
name: TName;
}
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
@@ -39,7 +46,7 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext.name) {
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
@@ -55,9 +62,9 @@ const useFormField = () => {
};
};
interface FormItemContextValue {
type FormItemContextValue = {
id: string;
}
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
@@ -75,57 +82,88 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ComponentRef<typeof LabelPrimitive.Root>,
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ComponentRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
);
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
);
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error.message) : children;
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
{body}
</p>
);
if (!body) {
return null;
}
);
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@@ -11,7 +11,7 @@ const labelVariants = cva(
);
const Label = React.forwardRef<
React.ComponentRef<typeof LabelPrimitive.Root>,
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />

View File

@@ -10,7 +10,7 @@ const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>

View File

@@ -13,7 +13,7 @@ const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Trigger>,
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
@@ -33,7 +33,7 @@ const SelectTrigger = React.forwardRef<
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
@@ -47,7 +47,7 @@ const SelectScrollUpButton = React.forwardRef<
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
@@ -61,7 +61,7 @@ const SelectScrollDownButton = React.forwardRef<
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Content>,
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
@@ -93,7 +93,7 @@ const SelectContent = React.forwardRef<
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Label>,
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
@@ -105,7 +105,7 @@ const SelectLabel = React.forwardRef<
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Item>,
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
@@ -128,7 +128,7 @@ const SelectItem = React.forwardRef<
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator

View File

@@ -1,37 +0,0 @@
// @ts-check
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import tseslint from "typescript-eslint";
const eslintConfig = defineConfig([
// Next.js core-web-vitals and TypeScript configs
...nextVitals,
...nextTs,
// Add strict TypeScript rules on top
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
// Configure TypeScript parser options
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// Override default ignores of eslint-config-next
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
// Additional ignores:
"*.mjs",
"tailwind.config.ts",
]),
]);
export default eslintConfig;

View File

@@ -1,8 +1,7 @@
{
"eventDates": ["2025-09-05", "2025-10-25", "2025-12-06", "1999-01-01"],
"cutoffTime": "23:00",
"blockDurationHours": 6,
"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 cutoffTime (23:00) for blockDurationHours (6 hours)."
"internalComment": "Add event dates in YYYY-MM-DD format. Signups will be disabled after 3pm (15:00) on these dates by default."
}

View File

@@ -1,12 +1,12 @@
import "server-only";
export interface listmonkData {
export type listmonkData = {
email: string;
name: string;
status: "enabled" | "blocklisted";
lists: number[];
attribs: Record<string, string>;
}
};
async function listmonk(data: listmonkData): Promise<string> {
const listmonkUrl = process.env.LISTMONK_URL ?? "http://localhost:9000/api/";

View File

@@ -1,40 +0,0 @@
import { isSignupBlocked } from "./signup-time-check";
// Test helper to check different scenarios
function testScenario(description: string, date: Date, expected: boolean) {
const result = isSignupBlocked(date);
const status = result.blocked === expected ? "✅ PASS" : "❌ FAIL";
console.log(`${status} ${description}`);
console.log(` Expected blocked: ${String(expected)}, Got: ${String(result.blocked)}`);
if (result.message) {
console.log(` Message: ${result.message}`);
}
console.log();
}
// Run tests
console.log("Testing signup blocking logic...\n");
// October 25, 2025 at 22:00 (before cutoff)
testScenario("Oct 25 at 22:00 - Before cutoff", new Date("2025-10-25T22:00:00"), false);
// October 25, 2025 at 23:00 (exactly at cutoff)
testScenario("Oct 25 at 23:00 - At cutoff time", new Date("2025-10-25T23:00:00"), true);
// October 25, 2025 at 23:30 (during block period)
testScenario("Oct 25 at 23:30 - During block period", new Date("2025-10-25T23:30:00"), true);
// October 26, 2025 at 02:00 (3 hours into block)
testScenario("Oct 26 at 02:00 - 3 hours into block", new Date("2025-10-26T02:00:00"), true);
// October 26, 2025 at 04:59 (just before block ends)
testScenario("Oct 26 at 04:59:59 - Just before block ends", new Date("2025-10-26T04:59:59"), true);
// October 26, 2025 at 05:00 (block period over)
testScenario("Oct 26 at 05:00 - Block period ended", new Date("2025-10-26T05:00:00"), false);
// October 26, 2025 at 12:00 (well after block)
testScenario("Oct 26 at 12:00 - Well after block", new Date("2025-10-26T12:00:00"), false);
// Random non-event date
testScenario("Oct 20 at 23:00 - Non-event date", new Date("2025-10-20T23:00:00"), false);

View File

@@ -1,23 +1,17 @@
import eventConfig from "@/event-dates.json";
export function isSignupBlocked(currentTime?: Date): { blocked: boolean; message?: string } {
const now = currentTime ?? new Date();
const cutoffTime = eventConfig.cutoffTime || "15:00";
const blockDurationHours = eventConfig.blockDurationHours || 6;
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 each event date to see if we're in a block period
for (const eventDate of eventConfig.eventDates) {
// Parse the event date and cutoff time in local timezone
const [year, month, day] = eventDate.split("-").map(Number);
const [hours, minutes] = cutoffTime.split(":").map(Number);
const blockStart = new Date(year, month - 1, day, hours, minutes, 0, 0);
// Check if today is an event date
const isEventDay = eventConfig.eventDates.includes(currentDate);
// Calculate when the block period ends (using wall-clock hours to handle DST correctly)
const blockEnd = new Date(blockStart);
blockEnd.setHours(blockStart.getHours() + blockDurationHours);
// Check if current time is within the block period
if (now >= blockStart && now < blockEnd) {
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,

View File

@@ -6,7 +6,7 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next typegen && eslint . && npx tsc --noEmit"
"check": "next lint && npx tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
@@ -18,38 +18,31 @@
"clsx": "^2.1.1",
"cssnano": "^7.1.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.554.0",
"next": "16.0.3",
"lucide-react": "^0.539.0",
"next": "^15.4.1",
"next-plausible": "^3.12.4",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^10.2.4",
"react": "19.2.0",
"react": "^19.1.0",
"react-day-picker": "^9.8.0",
"react-dom": "19.2.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.60.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.0.5"
},
"devDependencies": {
"@tailwindcss/postcss": "4.1.17",
"@types/node": "24.10.1",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"eslint": "9.39.1",
"eslint-config-next": "16.0.3",
"@tailwindcss/postcss": "4.1.11",
"@types/node": "22.17.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"eslint": "9.33.0",
"eslint-config-next": "15.4.5",
"eslint-config-prettier": "10.1.8",
"postcss": "8.5.6",
"tailwindcss": "4.1.17",
"turbo": "2.6.1",
"typescript": "5.9.3",
"typescript-eslint": "^8.47.0"
"tailwindcss": "4.1.11",
"turbo": "2.5.5",
"typescript": "5.9.2"
},
"packageManager": "pnpm@10.23.0",
"pnpm": {
"overrides": {
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3"
}
}
"packageManager": "pnpm@10.14.0"
}

2569
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{
@@ -22,13 +22,6 @@
},
"target": "ES2023"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"event-dates.json",
".next/dev/types/**/*.ts"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "event-dates.json"],
"exclude": ["node_modules"]
}