Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated Google Sheets, and added Team Name field to the schema #5

Merged
merged 5 commits into from
Jan 8, 2025
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Environment variables
.env
175 changes: 171 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useState } from 'react';
import { z } from 'zod';
import { useState, useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
Expand All @@ -6,9 +8,10 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select';
import { toast } from 'sonner';
import { formSchema } from '@/lib/schema';
import { SelectItems } from './SelectItems';
import saveData from './api/google-sheets';
import { z } from 'zod';
import { MatrixEffect } from './components/MatrixEffect';
Expand All @@ -22,6 +25,7 @@
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
teamName: '',
fullName: '',
rollNo: '',
email: '',
Expand Down Expand Up @@ -74,14 +78,27 @@
async function onSubmit(data: FormValues) {
setIsSubmitting(true);
try {
const response = await saveData(data);
const cleanedData = {
...data,
teamMembers: data.teamMembers.filter((member) => member !== undefined),
teamName: data.teamName,
};

const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/save-to-sheets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: cleanedData }),
});

if (!response.ok) throw new Error('Submission failed');
if (!response.ok) {
throw new Error('Submission failed');
}

toast.success('Form submitted successfully!');
form.reset();
setTeamMemberCount(0);
} catch (error) {
console.error('Error submitting form:', error);
toast.error('Failed to submit form. Please try again.');
} finally {
setIsSubmitting(false);
Expand All @@ -105,6 +122,156 @@
};

return (
<div className="min-h-screen bg-black flex items-center justify-center px-4 py-8 sm:px-6 lg:px-8">

Check failure on line 125 in src/App.tsx

View workflow job for this annotation

GitHub Actions / build

JSX expressions must have one parent element.
<div className="w-full max-w-3xl">
<Card className="bg-[#1a1a1a] border-[#333] shadow-2xl rounded-3xl">
<CardHeader className="space-y-2 border-b border-[#333] bg-gradient-to-r from-[#00ff80]/10 to-[#1d8a54]/10 rounded-t-3xl">
<CardTitle className="text-3xl font-bold bg-gradient-to-r from-[#00ff80] to-[#1d8a54] bg-clip-text text-transparent">
Registration Form
</CardTitle>
<CardDescription className="text-gray-400">
Fill in your details to register. You can participate solo or add up to 2 team members.
</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="teamName"
render={({ field }) => (
<FormItem>
<FormLabel className="text-white">Team Name</FormLabel>
<FormControl>
<Input
{...field}
value={typeof field.value === 'string' ? field.value : ''}
placeholder="Enter team name"
className="bg-[#222] border-[#333] text-white placeholder:text-gray-500 rounded-lg"
/>
</FormControl>
<FormMessage className="text-red-400" />
</FormItem>
)}
/>

{Object.keys(formSchema.shape).filter(key => key !== 'teamMembers' && key !== 'teamName').map((fieldName) => (
<FormField
key={fieldName}
control={form.control}
name={fieldName as keyof Omit<FormValues, 'teamMembers' | 'teamName'>}
render={({ field }) => (
<FormItem>
<FormLabel className="text-white">
{fieldName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</FormLabel>
<FormControl>
{fieldName === 'branch' ? (
<Select onValueChange={field.onChange} defaultValue={field.value as string}>
<FormControl>
<SelectTrigger className="bg-[#222] border-[#333] text-white rounded-lg">
<SelectValue placeholder={`Select ${fieldName}`} />
</SelectTrigger>
</FormControl>
<SelectContent className="bg-[#222] border-[#333] text-white rounded-lg">
<SelectItems />
</SelectContent>
</Select>
) : (
<Input
{...field}
value={typeof field.value === 'string' ? field.value : ''}
placeholder={`Enter ${fieldName.replace(/([A-Z])/g, ' $1').toLowerCase()}`}
className="bg-[#222] border-[#333] text-white placeholder:text-gray-500 rounded-lg"
/>
)}
</FormControl>
<FormMessage className="text-red-400" />
</FormItem>
)}
/>
))}
</div>

<div className="flex justify-between items-center pt-4">
<Button
type="button"
onClick={addTeamMember}
disabled={teamMemberCount >= 2}
className="bg-gradient-to-r from-[#00ff80] to-[#1d8a54] text-black hover:opacity-90 rounded-lg"
>
<UserPlus className="mr-2 h-4 w-4" />
Add Team Member
</Button>
{teamMemberCount > 0 && (
<Button
type="button"
onClick={removeTeamMember}
className="bg-[#222] text-white hover:bg-[#333] border-[#333] rounded-lg"
>
<X className="mr-2 h-4 w-4" />
Remove Member
</Button>
)}
</div>

{Array.from({ length: teamMemberCount }).map((_, index) => (
<div key={index} className="space-y-4 pt-4 border-t border-[#333]">
<h3 className="text-lg font-semibold text-[#00ff80]">Team Member {index + 1}</h3>
{Object.keys(formSchema.shape).filter(key => key !== 'teamMembers' && key !== 'teamName').map((fieldName) => (
<FormField
key={fieldName}
control={form.control}
name={`teamMembers.${index}.${fieldName}` as keyof FormValues}
render={({ field }) => (
<FormItem>
<FormLabel className="text-white">
{fieldName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</FormLabel>
<FormControl>
{fieldName === 'branch' ? (
<Select onValueChange={field.onChange} defaultValue={field.value as string}>
<FormControl>
<SelectTrigger className="bg-[#222] border-[#333] text-white rounded-lg">
<SelectValue placeholder={`Select ${fieldName}`} />
</SelectTrigger>
</FormControl>
<SelectContent className="bg-[#222] border-[#333] text-white rounded-lg">
<SelectItems />
</SelectContent>
</Select>
) : (
<Input
{...field}
value={typeof field.value === 'string' ? field.value : ''}
placeholder={`Enter ${fieldName.replace(/([A-Z])/g, ' $1').toLowerCase()}`}
className="bg-[#222] border-[#333] text-white placeholder:text-gray-500 rounded-lg"
/>
)}
</FormControl>
<FormMessage className="text-red-400" />
</FormItem>
)}
/>
))}
</div>
))}

<Button
type="submit"
disabled={isSubmitting}
className="w-full bg-gradient-to-r from-[#00ff80] to-[#1d8a54] hover:opacity-90 text-black font-semibold rounded-lg"
>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isSubmitting ? 'Submitting...' : 'Submit Registration'}
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
</div>
<>
<MatrixEffect />
<div className="circuit-decoration" />
Expand Down Expand Up @@ -245,4 +412,4 @@
</div>
</>
);
}
};
18 changes: 18 additions & 0 deletions src/SelectItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SelectItem } from '@/components/ui/select';

export const SelectItems = () => (
<>
<SelectItem value="cse">Computer Science</SelectItem>
<SelectItem value="csse">Computer Science and Systems Engineering</SelectItem>
<SelectItem value="csce">Computer Science and Communication Engineering</SelectItem>
<SelectItem value="etc">Electronics and Telecommunication Engineering</SelectItem>
<SelectItem value="ee">Electrical Engineering</SelectItem>
<SelectItem value="eee">Electronics and Electrical Engineering</SelectItem>
<SelectItem value="ecse">Electronics and Computer Science Engineering</SelectItem>
<SelectItem value="me">Mechanical Engineering</SelectItem>
<SelectItem value="mce">Mechantronics Engineering</SelectItem>
<SelectItem value="ce">Civil Engineering</SelectItem>
<SelectItem value="it">Information Technology</SelectItem>
<SelectItem value="ae">Aerospace Engineering</SelectItem>
</>
);
13 changes: 0 additions & 13 deletions src/api/google-sheets.ts

This file was deleted.

70 changes: 70 additions & 0 deletions src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import { DayPicker } from 'react-day-picker';

import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md'
),
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';

export { Calendar };
18 changes: 10 additions & 8 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import * as z from 'zod';

const baseFields = {
export const formSchema = z.object({
teamName: z.string().min(2, 'Team name must be at least 2 characters'),
fullName: z.string().min(2, 'Name must be at least 2 characters'),
rollNo: z.string().min(1, 'Roll number is required'),
email: z.string().email('Invalid email address'),
branch: z.string().min(1, 'Branch is required'),
};

const memberSchema = z.object(baseFields);

export const formSchema = z.object({
...baseFields,
teamMembers: z.array(memberSchema.optional()).max(2),
teamMembers: z.array(
z.object({
fullName: z.string().min(2, 'Name must be at least 2 characters'),
rollNo: z.string().min(1, 'Roll number is required'),
email: z.string().email('Invalid email address'),
branch: z.string().min(1, 'Branch is required'),
}).optional()
).max(2, "You can only have up to 2 team members"),
});
Loading