mirror of
https://github.com/harivansh-afk/RAG-ui.git
synced 2026-04-15 13:03:46 +00:00
added resources functionality
This commit is contained in:
parent
054abcee98
commit
39e3a77c08
7 changed files with 360 additions and 123 deletions
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
20
src/App.tsx
20
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 (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<AppRouter />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<AuthProvider>
|
||||
<Toaster position="top-right" />
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<AppRouter />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</SessionContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<Database>(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,
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Resource[]>([]);
|
||||
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 <div className="flex justify-center p-6">Loading...</div>;
|
||||
}
|
||||
|
||||
function StudyResources() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Study Resources</h1>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{mockResources.map((resource) => (
|
||||
<Card key={resource.id} className="p-4 hover:bg-muted/50 cursor-pointer">
|
||||
<div className="flex items-start space-x-4">
|
||||
{resource.type === 'folder' && <Folder className="h-6 w-6 text-blue-500" />}
|
||||
{resource.type === 'file' && <File className="h-6 w-6 text-green-500" />}
|
||||
{resource.type === 'book' && <Book className="h-6 w-6 text-purple-500" />}
|
||||
<div>
|
||||
<h3 className="font-medium">{resource.name}</h3>
|
||||
{resource.type === 'folder' && (
|
||||
<p className="text-sm text-muted-foreground">{resource.itemCount} items</p>
|
||||
)}
|
||||
{resource.type === 'file' && (
|
||||
<p className="text-sm text-muted-foreground">{resource.size}</p>
|
||||
)}
|
||||
{resource.type === 'book' && (
|
||||
<p className="text-sm text-muted-foreground">By {resource.author}</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{resources.map((resource) => (
|
||||
<Card key={resource.id} className="p-4 hover:bg-muted/50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{resource.resource_type === 'file' ? (
|
||||
<FileIcon className="h-5 w-5 text-primary" />
|
||||
) : (
|
||||
<LinkIcon className="h-5 w-5 text-primary" />
|
||||
)}
|
||||
<h3 className="font-medium truncate">{resource.title}</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(resource)}
|
||||
className="text-destructive hover:text-destructive/90 h-8 w-8 p-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mt-4 w-full justify-start text-muted-foreground hover:text-foreground"
|
||||
onClick={() => handleDownload(resource)}
|
||||
>
|
||||
{resource.resource_type === 'file' ? 'Download' : 'Open Link'}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{resources.length === 0 && (
|
||||
<div className="col-span-full text-center text-muted-foreground py-12">
|
||||
No resources found. Start by uploading some materials!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StudyResources;
|
||||
|
|
|
|||
|
|
@ -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<UploadType>('file');
|
||||
const [title, setTitle] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const supabase = useSupabaseClient();
|
||||
const user = useUser();
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Upload Study Materials</h1>
|
||||
<div className="rounded-lg bg-card p-6 shadow-sm">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 py-12">
|
||||
<Upload className="h-12 w-12 text-muted-foreground" />
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-semibold">Drop your files here</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
or click to browse from your computer
|
||||
</p>
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
variant={uploadType === 'file' ? 'default' : 'outline'}
|
||||
onClick={() => setUploadType('file')}
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
<Button
|
||||
variant={uploadType === 'link' ? 'default' : 'outline'}
|
||||
onClick={() => setUploadType('link')}
|
||||
>
|
||||
Add Link
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline">Choose Files</Button>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{uploadType === 'file' ? (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">File</label>
|
||||
<div className="mt-1.5">
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt,.ppt,.pptx"
|
||||
required
|
||||
className="flex w-full rounded-md border border-input bg-background text-sm ring-offset-background file:mr-4 file:py-2.5 file:px-4 file:mt-0 file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isUploading}
|
||||
className="w-full"
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Upload Resource'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UploadMaterials;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue