Skip to content

Commit

Permalink
basic dashboard page setup and added profile page for user info
Browse files Browse the repository at this point in the history
  • Loading branch information
aayank13 committed Jan 6, 2025
1 parent aa03d5c commit 765f385
Show file tree
Hide file tree
Showing 13 changed files with 566 additions and 45 deletions.
18 changes: 16 additions & 2 deletions app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import React from "react";
import { Metadata } from "next";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { Topbar } from "@/components/dashboard/Topbar";

export const metadata: Metadata = {
title: "Dashboard - ML4E",
description:
"ML4E - Machine Learning for Everyone is a collection of resources to help you learn machine learning.",
};

export default function Layout({ children }: { children: React.ReactNode }) {
return <main>{children}</main>;
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Topbar />
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
</div>
);
}
120 changes: 120 additions & 0 deletions app/dashboard/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Loader2, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { useToast } from "@/hooks/use-toast";
import { ToastAction } from "@/components/ui/toast";

const profileFormSchema = z.object({
fullName: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
});

export default function ProfilePage() {
const [isLoading, setIsLoading] = useState(false);
const { supabase, user } = useSupabase();
const { toast } = useToast();

const form = useForm<z.infer<typeof profileFormSchema>>({
resolver: zodResolver(profileFormSchema),
defaultValues: {
fullName: user?.user_metadata?.full_name || "",
email: user?.email || "",
},
});

async function onSubmit(values: z.infer<typeof profileFormSchema>) {
setIsLoading(true);
try {
const { error } = await supabase.auth.updateUser({
email: values.email,
data: { full_name: values.fullName },
});
if (error) throw error;
toast({
description: "Profile updated successfully",
});
} catch (error) {
toast({
variant: "destructive",
title: "Uh oh! Error",
description: "Error updating profile",
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
} finally {
setIsLoading(false);
}
}

return (
<div className="max-w-2xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={user?.user_metadata?.avatar_url} />
<AvatarFallback>
<User className="h-10 w-10" />
</AvatarFallback>
</Avatar>
<Button variant="outline">Change Avatar</Button>
</div>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="fullName"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
);
}
11 changes: 6 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "@/context/ThemeContext";
import { Analytics } from "@vercel/analytics/react"
import { Analytics } from "@vercel/analytics/react";
import { Toaster } from "@/components/ui/toaster";
import SupabaseProvider from "@/components/providers/SupabaseProvider";

export const metadata: Metadata = {
title: "ML4E - Machine Learning for Everyone",
Expand All @@ -18,10 +19,10 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<ThemeProvider>
{children}
</ThemeProvider>
<Toaster />
<SupabaseProvider>
<ThemeProvider>{children}</ThemeProvider>
<Toaster />
</SupabaseProvider>
<Analytics />
</body>
</html>
Expand Down
10 changes: 5 additions & 5 deletions components/auth/loginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
Expand All @@ -28,7 +27,6 @@ const formSchema = z.object({

export function LoginForm() {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const supabase = createClient();
const { toast } = useToast();

Expand Down Expand Up @@ -73,10 +71,12 @@ export function LoginForm() {

if (data?.user) {
toast({
description: "Successfully signed in",
description: "Successfully signed in! Redirecting...",
})
router.refresh();
router.push("/dashboard");
window.location.href = "/dashboard";
// setTimeout(() => {
// window.location.reload()
// }, 1000);
}
} catch (error) {
console.error("Login error:", error);
Expand Down
75 changes: 42 additions & 33 deletions components/auth/signupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
"use client";

import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { createClient } from '@/lib/supabase/client';
import { useToast } from "@/hooks/use-toast"
import { ToastAction } from "@/components/ui/toast"
import { OAuthButtons } from './oAuthButton';
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { createClient } from "@/lib/supabase/client";
import { useToast } from "@/hooks/use-toast";
import { ToastAction } from "@/components/ui/toast";
import { OAuthButtons } from "./oAuthButton";

const formSchema = z.object({
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(50, 'Name must be less than 50 characters'),
email: z.string().email('Invalid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
const formSchema = z
.object({
name: z
.string()
.min(2, "Name must be at least 2 characters")
.max(50, "Name must be less than 50 characters"),
email: z.string().email("Invalid email address"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});

export function SignupForm() {
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -43,7 +47,7 @@ export function SignupForm() {

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { name: '', email: '', password: '', confirmPassword: '' },
defaultValues: { name: "", email: "", password: "", confirmPassword: "" },
});

async function onSubmit(values: z.infer<typeof formSchema>) {
Expand All @@ -61,7 +65,10 @@ export function SignupForm() {
if (error) throw error;
toast({
description: "Check your email to confirm your account",
})
});
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
toast({
variant: "destructive",
Expand All @@ -76,7 +83,6 @@ export function SignupForm() {

return (
<div className="w-full space-y-6">

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
Expand Down Expand Up @@ -131,7 +137,11 @@ export function SignupForm() {
</FormItem>
)}
/>
<Button type="submit" className="w-full bg-indigo-600 hover:bg-indigo-800" disabled={isLoading}>
<Button
type="submit"
className="w-full bg-indigo-600 hover:bg-indigo-800"
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Account
</Button>
Expand All @@ -150,7 +160,6 @@ export function SignupForm() {
</div>

<OAuthButtons />

</div>
);
}
}
59 changes: 59 additions & 0 deletions components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { LayoutDashboard, Users, Database, GraduationCap, Cpu } from 'lucide-react'
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { UserNav } from './UserNav';

const sidebarItems = [
{ icon: LayoutDashboard, label: 'Dashboard', href: '/dashboard' },
{ icon: Users, label: 'Community', href: '/dashboard/community' },
{ icon: Database, label: 'Datasets', href: '/dashboard/datasets' },
{ icon: GraduationCap, label: 'Learn', href: '/dashboard/learn' },
{ icon: Cpu, label: 'Models', href: '/dashboard/models' },
]

export function Sidebar() {
const pathname = usePathname();
const [isCollapsed] = useState(false);

return (
<div className={cn(
"flex flex-col h-screen border-r bg-card",
isCollapsed ? "w-16" : "w-64"
)}>
<div className="p-4 flex items-center gap-2">
<Image src="/logo.png" alt="ML4E" width={32} height={32} />
{!isCollapsed && <span className="font-bold text-lg">ML4E</span>}
</div>

<nav className="flex-1 p-2 space-y-1">
{sidebarItems.map((item) => {
const isActive = pathname.startsWith(item.href);
return (
<Link key={item.href} href={item.href}>
<Button
variant={isActive ? "secondary" : "ghost"}
className={cn(
"w-full justify-start",
isCollapsed && "justify-center"
)}
>
<item.icon className="h-5 w-5" />
{!isCollapsed && <span className="ml-2">{item.label}</span>}
</Button>
</Link>
);
})}
</nav>

<div className="p-4 border-t">
<UserNav isCollapsed={isCollapsed} />
</div>
</div>
);
}
Loading

0 comments on commit 765f385

Please sign in to comment.