From 8ec33f1f3b261529cfda6369bb74b1bb06f3b161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matica=20Lauren=C8=9Biu-Andrei?= Date: Sat, 8 Nov 2025 22:25:25 +0200 Subject: [PATCH 1/3] css & animations --- .claude/settings.local.json | 3 +- frontend/app/dashboard/page.tsx | 28 ++- .../dashboard/student/homework/[id]/page.tsx | 17 +- frontend/app/dashboard/student/page.tsx | 202 ++++++++++-------- .../app/dashboard/student/task/[id]/page.tsx | 15 +- frontend/app/dashboard/teacher/page.tsx | 72 ++++--- frontend/app/globals.css | 190 ++++++++++++++++ frontend/app/page.tsx | 108 ++++++---- frontend/app/providers.tsx | 5 +- frontend/app/settings/page.tsx | 81 ++++--- frontend/app/test-toast/page.tsx | 106 +++++++++ frontend/components/DataPrivacyPanel.tsx | 52 +++-- frontend/components/ui/loading-spinner.tsx | 56 +++++ frontend/components/ui/toast.tsx | 170 +++++++++++++++ frontend/lib/animations.ts | 43 ++++ 15 files changed, 917 insertions(+), 231 deletions(-) create mode 100644 frontend/app/test-toast/page.tsx create mode 100644 frontend/components/ui/loading-spinner.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/lib/animations.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 03b0f28..e2d3b5e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "allow": [ "Bash(npm run dev:*)", "Bash(npm run build:*)", - "Bash(timeout 30 npm run dev:*)" + "Bash(timeout 30 npm run dev:*)", + "Bash(cat:*)" ], "deny": [], "ask": [] diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 0922fe6..5f18248 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -65,8 +65,18 @@ export default function DashboardPage() { if (loading) { return ( -
- +
+
+
+
+
+
+
+ +
+
+

Loading your dashboard...

+
); } @@ -77,8 +87,18 @@ export default function DashboardPage() { // Show loading while redirecting return ( -
- +
+
+
+
+
+
+
+ +
+
+

Redirecting...

+
); } diff --git a/frontend/app/dashboard/student/homework/[id]/page.tsx b/frontend/app/dashboard/student/homework/[id]/page.tsx index 26d50f4..2064355 100644 --- a/frontend/app/dashboard/student/homework/[id]/page.tsx +++ b/frontend/app/dashboard/student/homework/[id]/page.tsx @@ -19,6 +19,8 @@ import { Upload, FileText, CheckCircle, MessageCircle, Loader2, ArrowLeft, Downl import { Input } from '@/components/ui/input'; import Link from 'next/link'; import { createSupabaseBrowserClient } from '@/lib/supabase/client'; +import { useToast } from '@/components/ui/toast'; +import { LoadingPage } from '@/components/ui/loading-spinner'; const supabase = createSupabaseBrowserClient(); @@ -26,6 +28,7 @@ export default function StudentHomeworkPage() { const { address, isConnected } = useAccount(); const router = useRouter(); const params = useParams(); + const toast = useToast(); const homeworkId = params.id as string; const [profile, setProfile] = useState(null); @@ -74,7 +77,7 @@ export default function StudentHomeworkPage() { }); if (enrollmentsData.length === 0) { - alert('You are not enrolled in this task'); + toast.warning('Not enrolled', 'You are not enrolled in this task'); router.push('/dashboard/student'); return; } @@ -105,7 +108,7 @@ export default function StudentHomeworkPage() { setTaskResources(resourcesData); } catch (error: any) { console.error('Error loading data:', error); - alert('Error loading task details'); + toast.error('Loading failed', 'Error loading task details'); router.push('/dashboard/student'); } finally { setLoading(false); @@ -113,7 +116,7 @@ export default function StudentHomeworkPage() { } loadData(); - }, [address, isConnected, router, homeworkId]); + }, [address, isConnected, router, homeworkId, toast]); async function handleSubmitSolution() { if (!enrollment || !submissionText.trim()) return; @@ -139,10 +142,10 @@ export default function StudentHomeworkPage() { }); setEnrollment(enrollmentsData[0]); - alert('Solution submitted successfully! ✅ Your teacher will review it soon.'); + toast.success('Solution submitted!', 'Your teacher will review it soon.'); } catch (error: any) { console.error('Error submitting solution:', error); - alert('Error submitting solution. Please try again.'); + toast.error('Submission failed', 'Error submitting solution. Please try again.'); } finally { setSubmitting(false); } @@ -187,10 +190,10 @@ export default function StudentHomeworkPage() { setSubmissions(submissionsData); setUploadedFile(null); - alert('File uploaded successfully! ✅'); + toast.success('File uploaded!', 'Your file has been uploaded successfully.'); } catch (error: any) { console.error('Error uploading file:', error); - alert('Error uploading file. Please try again.'); + toast.error('Upload failed', 'Error uploading file. Please try again.'); } finally { setUploading(false); } diff --git a/frontend/app/dashboard/student/page.tsx b/frontend/app/dashboard/student/page.tsx index 824e706..5c879f5 100644 --- a/frontend/app/dashboard/student/page.tsx +++ b/frontend/app/dashboard/student/page.tsx @@ -24,15 +24,20 @@ import { CheckCircle, Award, Eye, + User, } from 'lucide-react'; import Link from 'next/link'; import { createSupabaseBrowserClient } from '@/lib/supabase/client'; +import { useToast } from '@/components/ui/toast'; +import { LoadingPage } from '@/components/ui/loading-spinner'; +import { cn } from '@/lib/utils'; const supabase = createSupabaseBrowserClient(); export default function StudentDashboard() { const { address, isConnected } = useAccount(); const router = useRouter(); + const toast = useToast(); const [profile, setProfile] = useState(null); const [availableHomeworks, setAvailableHomeworks] = useState([]); const [myEnrollments, setMyEnrollments] = useState([]); @@ -111,13 +116,13 @@ export default function StudentDashboard() { const homeworksData = await getAvailableHomeworks(); setAvailableHomeworks(homeworksData); - alert('Enrolled successfully! ✅'); + toast.success('Enrolled successfully!', 'You can now start working on this task.'); } catch (error: any) { console.error('Error enrolling:', error); if (error.message?.includes('duplicate')) { - alert('You are already enrolled in this task!'); + toast.warning('Already enrolled', 'You are already enrolled in this task!'); } else { - alert('Error enrolling. Please try again.'); + toast.error('Enrollment failed', 'Error enrolling. Please try again.'); } } finally { setEnrolling(null); @@ -125,7 +130,7 @@ export default function StudentDashboard() { } if (loading) { - return
Loading...
; + return ; } if (!profile) { @@ -135,96 +140,107 @@ export default function StudentDashboard() { const unansweredQuestions = myQuestions.filter(q => !q.is_answered).length; return ( -
-
-
-

Student Dashboard

-

- Browse tasks, ask questions, and learn! -

-
+
+ {/* Animated background blobs */} +
+
+
+
+ +
+
+
+

+ Student Dashboard +

+

+ Browse tasks, ask questions, and learn! 📚 +

+
- {/* Mentor Eligibility Banner */} - {canBecomeMentor && ( - - -
-
- -
-

- 🎉 You're eligible to become a Mentor! -

-

- You have {profile?.rating?.toFixed(1) || '0.0'}+ stars and {profile?.completed_count || 0}+ - completed tasks -

+ {/* Mentor Eligibility Banner */} + {canBecomeMentor && ( + + +
+
+
+ +
+
+

+ 🎉 You're eligible to become a Mentor! +

+

+ You have {profile?.rating?.toFixed(1) || '0.0'}+ stars and {profile?.completed_count || 0}+ + completed tasks +

+
+ + +
- - - -
- - - )} + + + )} - {/* Stats Cards */} -
- - - My Enrollments - - - -
{myEnrollments.length}
-

active tasks

-
-
+ {/* Stats Cards */} +
+ + + My Enrollments + + + +
{myEnrollments.length}
+

active tasks

+
+
- - - Completed - - - -
{profile?.completed_count || 0}
-

tasks finished

-
-
+ + + Completed + + + +
{profile?.completed_count || 0}
+

tasks finished

+
+
- - - My Rating - - - -
{profile?.rating?.toFixed(1) || '0.0'}/5
-

- {profile?.total_reviews || 0} {(profile?.total_reviews || 0) === 1 ? 'review' : 'reviews'} -

-
-
+ + + My Rating + + + +
{profile?.rating?.toFixed(1) || '0.0'}/5
+

+ {profile?.total_reviews || 0} {(profile?.total_reviews || 0) === 1 ? 'review' : 'reviews'} +

+
+
- - - Token Balance - - - -
{profile?.token_balance || 0}
-

tokens

-
-
-
+ + + Token Balance + + + +
{profile?.token_balance || 0}
+

tokens

+
+
+
- {/* My Enrollments */} - {myEnrollments.length > 0 && ( -
-

My Enrollments

+ {/* My Enrollments */} + {myEnrollments.length > 0 && ( +
+

My Enrollments

{myEnrollments.map((enrollment) => { return ( @@ -283,9 +299,9 @@ export default function StudentDashboard() {
)} - {/* Available Tasks */} -
-

Available Tasks

+ {/* Available Tasks */} +
+

Available Tasks

{availableHomeworks.length === 0 ? ( @@ -345,7 +361,12 @@ export default function StudentDashboard() {
+
); } diff --git a/frontend/app/dashboard/student/task/[id]/page.tsx b/frontend/app/dashboard/student/task/[id]/page.tsx index 2cee15d..4d4a411 100644 --- a/frontend/app/dashboard/student/task/[id]/page.tsx +++ b/frontend/app/dashboard/student/task/[id]/page.tsx @@ -15,6 +15,8 @@ import type { Homework, TaskResource } from '@/lib/types/database'; import { ArrowLeft, Download, FileText, Users, Loader2 } from 'lucide-react'; import Link from 'next/link'; import { createSupabaseBrowserClient } from '@/lib/supabase/client'; +import { useToast } from '@/components/ui/toast'; +import { LoadingPage } from '@/components/ui/loading-spinner'; const supabase = createSupabaseBrowserClient(); @@ -22,6 +24,7 @@ export default function StudentTaskViewPage() { const { address, isConnected } = useAccount(); const router = useRouter(); const params = useParams(); + const toast = useToast(); const homeworkId = params.id as string; const [profile, setProfile] = useState(null); @@ -74,7 +77,7 @@ export default function StudentTaskViewPage() { setTaskResources(resourcesData); } catch (error: any) { console.error('Error loading data:', error); - alert('Error loading task details'); + toast.error('Loading failed', 'Error loading task details'); router.push('/dashboard/student'); } finally { setLoading(false); @@ -82,7 +85,7 @@ export default function StudentTaskViewPage() { } loadData(); - }, [address, isConnected, router, homeworkId]); + }, [address, isConnected, router, homeworkId, toast]); async function handleEnroll() { if (!profile || !homework) return; @@ -90,14 +93,14 @@ export default function StudentTaskViewPage() { setEnrolling(true); try { await enrollInHomework(profile.id, homework.id); - alert('Enrolled successfully! ✅'); + toast.success('Enrolled successfully!', 'Redirecting to your task...'); router.push(`/dashboard/student/homework/${homework.id}`); } catch (error: any) { console.error('Error enrolling:', error); if (error.message?.includes('duplicate')) { - alert('You are already enrolled in this task!'); + toast.warning('Already enrolled', 'You are already enrolled in this task!'); } else { - alert('Error enrolling. Please try again.'); + toast.error('Enrollment failed', 'Error enrolling. Please try again.'); } } finally { setEnrolling(false); @@ -105,7 +108,7 @@ export default function StudentTaskViewPage() { } if (loading) { - return
Loading...
; + return ; } if (!profile || !homework) { diff --git a/frontend/app/dashboard/teacher/page.tsx b/frontend/app/dashboard/teacher/page.tsx index 68975be..c68a863 100644 --- a/frontend/app/dashboard/teacher/page.tsx +++ b/frontend/app/dashboard/teacher/page.tsx @@ -103,36 +103,51 @@ export default function TeacherDashboard() { } return ( -
-
-
-

Teacher Dashboard

-

- Manage tasks, answer questions, and review students -

-
- - - +
+ {/* Animated background blobs */} +
+
+
- {/* Token Balance Warning */} - {profile.token_balance < 1 && ( - - -
- -

- Insufficient tokens! You need at least 1 token to create a task. - Each task costs 1 token. -

-
-
-
- )} +
+
+
+

+ Teacher Dashboard +

+

+ Manage tasks, answer questions, and review students 👨‍🏫 +

+
+ + + +
+ + {/* Token Balance Warning */} + {profile.token_balance < 1 && ( + + +
+
+ +
+

+ Insufficient tokens! You need at least 1 token to create a task. + Each task costs 1 token. +

+
+
+
+ )} {/* Stats Cards */}
@@ -290,5 +305,6 @@ export default function TeacherDashboard() { )}
+
); } diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 4fced5d..6bbf83c 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -120,3 +120,193 @@ @apply bg-background text-foreground; } } + +@layer utilities { + /* MetaMask-inspired animations */ + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + @keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } + } + + @keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } + } + + @keyframes pulse-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); + } + 50% { + box-shadow: 0 0 30px rgba(59, 130, 246, 0.5); + } + } + + @keyframes progress { + from { + width: 100%; + } + to { + width: 0%; + } + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + @keyframes bounce-in { + 0% { + transform: scale(0.3); + opacity: 0; + } + 50% { + transform: scale(1.05); + } + 70% { + transform: scale(0.9); + } + 100% { + transform: scale(1); + opacity: 1; + } + } + + .animate-fade-in { + animation: fadeIn 0.5s ease-out; + } + + .animate-slide-in { + animation: slideIn 0.4s ease-out; + } + + .animate-scale-in { + animation: scaleIn 0.3s ease-out; + } + + .animate-shimmer { + background: linear-gradient( + to right, + transparent 0%, + rgba(255, 255, 255, 0.1) 50%, + transparent 100% + ); + background-size: 1000px 100%; + animation: shimmer 2s infinite; + } + + .animate-spin { + animation: spin 1s linear infinite; + } + + .animate-bounce-in { + animation: bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); + } + + /* Glass morphism effect */ + .glass { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .glass-dark { + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + /* Gradient borders */ + .gradient-border { + position: relative; + background: linear-gradient(var(--background), var(--background)) padding-box, + linear-gradient(135deg, #667eea 0%, #764ba2 100%) border-box; + border: 2px solid transparent; + } + + /* Hover effects */ + .card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .card-hover:hover { + transform: translateY(-4px); + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + } + + /* MetaMask-style button */ + .metamask-button { + background: linear-gradient(135deg, #f6851b 0%, #e2761b 100%); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); + transition: all 0.2s ease; + } + + .metamask-button:hover { + background: linear-gradient(135deg, #e2761b 0%, #cd6116 100%); + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.2); + transform: translateY(-1px); + } + + /* Smooth transitions */ + .smooth-transition { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + /* Loading skeleton */ + @keyframes skeleton-loading { + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } + } + + .skeleton { + background: linear-gradient( + 90deg, + rgba(226, 232, 240, 0.2) 0px, + rgba(226, 232, 240, 0.3) 40px, + rgba(226, 232, 240, 0.2) 80px + ); + background-size: 200px 100%; + animation: skeleton-loading 1.4s ease-in-out infinite; + } +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index b5e569c..d023e9a 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -42,46 +42,52 @@ export default function Home() { return (
{/* Hero Section */} -
-
-
+
+ {/* Animated background blobs */} +
+
+
+
+
+ +
- - + + Education in the Era of Technology -

+

Learn, Stake, and Earn in Web3

-

+

A decentralized platform where teachers create educational tasks and students solve them. - Fair incentives through staking, AI-powered personalization, and complete transparency. + Fair incentives through staking, AI-powered personalization, and complete transparency. ✨

-
+
- -
{/* Stats */} -
- {stats.map((stat) => ( -
-
+
+ {stats.map((stat, index) => ( +
+
{stat.value}
-
+
{stat.label}
@@ -102,16 +108,25 @@ export default function Home() {
- {features.map((feature) => { + {features.map((feature, index) => { const Icon = feature.icon; + const gradients = [ + 'from-blue-500 to-cyan-500', + 'from-green-500 to-emerald-500', + 'from-purple-500 to-pink-500', + 'from-orange-500 to-red-500' + ]; return ( - - -
- + +
+ +
+
- {feature.title} - + + {feature.title} + + {feature.description}
@@ -133,31 +148,34 @@ export default function Home() {
-
-
- 1 +
+
+
+ 1
-

Connect Wallet

+

Connect Wallet

Connect your MetaMask or any Web3 wallet to get started

-
-
- 2 +
+
+
+ 2
-

Choose Your Role

+

Choose Your Role

Be a teacher creating tasks or a student solving challenges

-
-
- 3 +
+
+
+ 3
-

Stake & Learn

+

Stake & Learn

Stake tokens, complete tasks, and earn rewards through learning

@@ -213,15 +231,21 @@ export default function Home() {
{/* CTA */} -
-
-

Ready to Start Learning?

-

+

+
+
+
+
+
+
+

Ready to Start Learning? 🚀

+

Join thousands of students and teachers already on the platform

-
diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx index bf8f519..9b6158f 100644 --- a/frontend/app/providers.tsx +++ b/frontend/app/providers.tsx @@ -4,6 +4,7 @@ import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; import { wagmiConfig } from '@/lib/wagmi'; +import { ToastProvider } from '@/components/ui/toast'; import React from 'react'; const queryClient = new QueryClient(); @@ -13,7 +14,9 @@ export function Providers({ children }: { children: React.ReactNode }) { - {children} + + {children} + diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index c5afe25..76c4c1f 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -13,11 +13,13 @@ import { Select } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { User, Settings as SettingsIcon } from 'lucide-react'; import { createSupabaseBrowserClient } from '@/lib/supabase/client'; +import { useToast } from '@/components/ui/toast'; const supabase = createSupabaseBrowserClient(); export default function SettingsPage() { const { address } = useAccount(); + const toast = useToast(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -60,10 +62,10 @@ export default function SettingsPage() { setSaving(true); try { await updateProfile(profile.id, formData); - alert('Profile updated successfully!'); + toast.success('Profile updated successfully!', 'Your changes have been saved.'); } catch (error) { console.error('Error updating profile:', error); - alert('Error updating profile. Please try again.'); + toast.error('Error updating profile', 'Please try again later.'); } finally { setSaving(false); } @@ -86,21 +88,31 @@ export default function SettingsPage() { } return ( -
-
-

Settings

-

- Manage your profile, privacy settings, and understand how your data is used +

+ {/* Animated background blobs */} +
+
+
+
+ +
+

+ Settings +

+

+ Manage your profile, privacy settings, and understand how your data is used ⚙️

-
+
{/* Profile Settings */} - +
- - Profile Settings +
+ +
+ Profile Settings
Update your personal information and preferences
@@ -123,7 +135,7 @@ export default function SettingsPage() { value={formData.username} onChange={(e) => setFormData({ ...formData, username: e.target.value })} placeholder="Enter your username" - className="w-full px-4 py-2 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-4 py-2 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 focus:outline-none focus:ring-2 focus:ring-blue-500 smooth-transition hover:border-blue-400" />
@@ -132,7 +144,7 @@ export default function SettingsPage() {