diff --git a/package-lock.json b/package-lock.json index 66c9345..c8a8618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", - "@supabase/supabase-js": "^2.47.0", + "@supabase/auth-helpers-react": "^0.5.0", + "@supabase/supabase-js": "^2.47.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.4.1", "react-router-dom": "^6.22.3", "tailwind-merge": "^2.2.1" }, @@ -2029,6 +2031,15 @@ "win32" ] }, + "node_modules/@supabase/auth-helpers-react": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-react/-/auth-helpers-react-0.5.0.tgz", + "integrity": "sha512-5QSaV2CGuhDhd7RlQCoviVEAYsP7XnrFMReOcBazDvVmqSIyjKcDwhLhWvnrxMOq5qjOaA44MHo7wXqDiF0puQ==", + "license": "MIT", + "peerDependencies": { + "@supabase/supabase-js": "^2.39.8" + } + }, "node_modules/@supabase/auth-js": { "version": "2.65.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", @@ -2069,9 +2080,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", - "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.9.tgz", + "integrity": "sha512-0AjN65VDNIScZzrrPaVvlND4vbgVS+j9Wcy3zf7e+l9JY4IwCTahFenPLcKy9bkr7KY0wfB7MkipZPKxMaDnjw==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "^2.6.14", @@ -2090,16 +2101,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.47.0.tgz", - "integrity": "sha512-SvLKFI21m3b1NAbc8nvZbOhHihUtujhUaR5TqYxdt52ag6moUJnQNVt0l0Upw3z0BMuG/YOIuI3hMzuo1EqUEw==", + "version": "2.47.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.47.1.tgz", + "integrity": "sha512-Q5zBX3BhK4tIFE6W8TK7NP29G9XCX8nzMydHONI8e/6HjYnKmUyNf33rsTTwGVVlXz2ruzJKO+EX4wuwD21Q4g==", "license": "MIT", "dependencies": { "@supabase/auth-js": "2.65.1", "@supabase/functions-js": "2.4.3", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.16.3", - "@supabase/realtime-js": "2.11.2", + "@supabase/realtime-js": "2.10.9", "@supabase/storage-js": "2.7.1" } }, @@ -2838,8 +2849,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/debug": { "version": "4.3.7", @@ -3456,6 +3466,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3762,6 +3781,7 @@ "version": "0.344.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -4255,6 +4275,22 @@ "react": "^18.3.1" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "license": "MIT", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/package.json b/package.json index b72e14d..2193734 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,14 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", - "@supabase/supabase-js": "^2.47.0", + "@supabase/auth-helpers-react": "^0.5.0", + "@supabase/supabase-js": "^2.47.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.4.1", "react-router-dom": "^6.22.3", "tailwind-merge": "^2.2.1" }, diff --git a/src/App.tsx b/src/App.tsx index 9058e19..3e74fa2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,24 @@ import React from 'react'; import { BrowserRouter } from 'react-router-dom'; import { AppRouter } from './routes'; +import { SessionContextProvider } from '@supabase/auth-helpers-react'; +import { Toaster } from 'react-hot-toast'; +import { supabase } from './lib/supabase'; import { AuthProvider } from './contexts/AuthContext'; import './index.css'; const App: React.FC = () => { return ( - - -
- -
-
-
+ + + + +
+ +
+
+
+
); }; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index edc1736..ac860b0 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { Session, User, AuthError } from '@supabase/supabase-js'; -import { auth } from '../lib/supabase'; +import { supabase } from '../lib/supabase'; interface AuthContextType { session: Session | null; @@ -20,14 +20,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useEffect(() => { // Get initial session - auth.getSession().then(({ session: initialSession }) => { + supabase.auth.getSession().then(({ data: { session: initialSession } }) => { setSession(initialSession); setUser(initialSession?.user ?? null); setLoading(false); }); // Listen for auth changes - const { data: { subscription } } = auth.onAuthStateChange((_event, session) => { + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); setUser(session?.user ?? null); setLoading(false); @@ -43,15 +43,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { user, loading, signIn: async (email: string, password: string) => { - const { error } = await auth.signIn(email, password); + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); return { error }; }, signUp: async (email: string, password: string) => { - const { error } = await auth.signUp(email, password); + const { error } = await supabase.auth.signUp({ + email, + password, + }); return { error }; }, signOut: async () => { - const { error } = await auth.signOut(); + const { error } = await supabase.auth.signOut(); return { error }; }, }; diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index d62c64a..28a226c 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,5 +1,4 @@ -import { createClient, AuthError, Session, AuthResponse } from '@supabase/supabase-js'; -import type { Database } from '../types/supabase'; +import { createClient } from '@supabase/supabase-js'; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; @@ -8,37 +7,9 @@ if (!supabaseUrl || !supabaseAnonKey) { throw new Error('Missing Supabase environment variables'); } -export const supabase = createClient(supabaseUrl, supabaseAnonKey); - -// Auth helper functions -export const auth = { - signUp: async (email: string, password: string): Promise<{ data: AuthResponse | null; error: AuthError | null }> => { - const { data, error } = await supabase.auth.signUp({ - email, - password, - }); - return { data: data as AuthResponse | null, error }; - }, - - signIn: async (email: string, password: string): Promise<{ data: AuthResponse | null; error: AuthError | null }> => { - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - return { data: data as AuthResponse | null, error }; - }, - - signOut: async (): Promise<{ error: AuthError | null }> => { - const { error } = await supabase.auth.signOut(); - return { error }; - }, - - getSession: async (): Promise<{ session: Session | null; error: AuthError | null }> => { - const { data: { session }, error } = await supabase.auth.getSession(); - return { session, error }; - }, - - onAuthStateChange: (callback: (event: string, session: Session | null) => void) => { - return supabase.auth.onAuthStateChange(callback); - }, -}; +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: true, + autoRefreshToken: true, + } +}); diff --git a/src/pages/dashboard/resources.tsx b/src/pages/dashboard/resources.tsx index 63669b3..9c148b8 100644 --- a/src/pages/dashboard/resources.tsx +++ b/src/pages/dashboard/resources.tsx @@ -1,63 +1,148 @@ -import React from 'react'; -import { Book, File, Folder } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useSupabaseClient, useUser } from '@supabase/auth-helpers-react'; +import { FileIcon, LinkIcon, Trash2 } from 'lucide-react'; +import { toast } from 'react-hot-toast'; import { Card } from '../../components/ui/card'; +import { Button } from '../../components/ui/Button'; -const mockResources = [ - { - id: '1', - type: 'folder', - name: 'Biology', - itemCount: 12, - }, - { - id: '2', - type: 'folder', - name: 'Physics', - itemCount: 8, - }, - { - id: '3', - type: 'file', - name: 'Math Notes.pdf', - size: '2.4 MB', - }, - { - id: '4', - type: 'book', - name: 'Chemistry Textbook', - author: 'John Smith', - }, -]; +interface Resource { + id: string; + title: string; + resource_type: 'file' | 'link'; + url?: string; + file_path?: string; + file_type?: string; + created_at: string; +} + +export default function ResourcesPage() { + const [resources, setResources] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const supabase = useSupabaseClient(); + const user = useUser(); + + useEffect(() => { + if (user) { + fetchResources(); + } + }, [user]); + + const fetchResources = async () => { + try { + const { data, error } = await supabase + .from('study_resources') + .select('*') + .eq('user_id', user?.id) + .order('created_at', { ascending: false }); + + if (error) throw error; + setResources(data || []); + } catch (error) { + console.error('Error fetching resources:', error); + toast.error('Failed to load resources'); + } finally { + setIsLoading(false); + } + }; + + const handleDownload = async (resource: Resource) => { + if (resource.resource_type === 'file' && resource.file_path) { + try { + const { data, error } = await supabase.storage + .from('study-materials') + .download(resource.file_path); + + if (error) throw error; + + const url = URL.createObjectURL(data); + const a = document.createElement('a'); + a.href = url; + a.download = resource.file_path.split('/').pop() || 'download'; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error('Download error:', error); + toast.error('Failed to download file'); + } + } else if (resource.resource_type === 'link' && resource.url) { + window.open(resource.url, '_blank'); + } + }; + + const handleDelete = async (resource: Resource) => { + try { + if (resource.resource_type === 'file' && resource.file_path) { + const { error: storageError } = await supabase.storage + .from('study-materials') + .remove([resource.file_path]); + + if (storageError) throw storageError; + } + + const { error: dbError } = await supabase + .from('study_resources') + .delete() + .eq('id', resource.id); + + if (dbError) throw dbError; + + setResources(resources.filter(r => r.id !== resource.id)); + toast.success('Resource deleted successfully'); + } catch (error) { + console.error('Delete error:', error); + toast.error('Failed to delete resource'); + } + }; + + if (isLoading) { + return
Loading...
; + } -function StudyResources() { return (

Study Resources

-
- {mockResources.map((resource) => ( - -
- {resource.type === 'folder' && } - {resource.type === 'file' && } - {resource.type === 'book' && } -
-

{resource.name}

- {resource.type === 'folder' && ( -

{resource.itemCount} items

- )} - {resource.type === 'file' && ( -

{resource.size}

- )} - {resource.type === 'book' && ( -

By {resource.author}

+ +
+ {resources.map((resource) => ( + +
+
+ {resource.resource_type === 'file' ? ( + + ) : ( + )} +

{resource.title}

+
+ +
))} + + {resources.length === 0 && ( +
+ No resources found. Start by uploading some materials! +
+ )}
); } - -export default StudyResources; diff --git a/src/pages/dashboard/upload.tsx b/src/pages/dashboard/upload.tsx index 7075cd0..b2c966c 100644 --- a/src/pages/dashboard/upload.tsx +++ b/src/pages/dashboard/upload.tsx @@ -1,25 +1,156 @@ -import React from 'react'; -import { Upload } from 'lucide-react'; +import { useState, ChangeEvent } from 'react'; +import { useSupabaseClient, useUser } from '@supabase/auth-helpers-react'; +import { toast } from 'react-hot-toast'; import { Button } from '../../components/ui/Button'; +import { Card } from '../../components/ui/card'; + +type UploadType = 'file' | 'link'; + +export default function UploadPage() { + const [uploadType, setUploadType] = useState('file'); + const [title, setTitle] = useState(''); + const [url, setUrl] = useState(''); + const [file, setFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + const supabase = useSupabaseClient(); + const user = useUser(); + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setFile(e.target.files[0]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!user) return; + + try { + setIsUploading(true); + + if (uploadType === 'file' && file) { + // Upload file to Supabase Storage + const fileExt = file.name.split('.').pop(); + const fileName = `${Math.random()}.${fileExt}`; + const filePath = `${user.id}/${fileName}`; + + const { error: uploadError } = await supabase.storage + .from('study-materials') + .upload(filePath, file); + + if (uploadError) throw uploadError; + + // Create database entry + const { error: dbError } = await supabase + .from('study_resources') + .insert({ + user_id: user.id, + title, + resource_type: 'file', + file_path: filePath, + file_type: file.type, + file_size: file.size + }); + + if (dbError) throw dbError; + } else if (uploadType === 'link') { + // Create database entry for link + const { error: dbError } = await supabase + .from('study_resources') + .insert({ + user_id: user.id, + title, + resource_type: 'link', + url + }); + + if (dbError) throw dbError; + } + + toast.success('Resource uploaded successfully!'); + // Reset form + setTitle(''); + setUrl(''); + setFile(null); + } catch (error) { + console.error('Upload error:', error); + toast.error('Failed to upload resource'); + } finally { + setIsUploading(false); + } + }; -function UploadMaterials() { return (

Upload Study Materials

-
-
- -
-

Drop your files here

-

- or click to browse from your computer -

+ +
+
+ +
- + +
+
+ + ) => setTitle(e.target.value)} + required + placeholder="Enter resource title" + 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 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + /> +
+ + {uploadType === 'file' ? ( +
+ +
+ +
+
+ ) : ( +
+ + ) => setUrl(e.target.value)} + required + placeholder="Enter resource URL" + 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 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + /> +
+ )} + + +
-
+
); } - -export default UploadMaterials;