From c422a6186ea78506f258ad09b2456a413ca4830b Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 10 Jan 2025 21:41:41 -0300 Subject: [PATCH] feat: implementa interesses do aluno e melhora responsividade dos menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona nova aba de Interesses nas configura����es do aluno - Implementa sistema de notifica����es toast usando Radix UI - Torna menus laterais responsivos e colaps��veis - Adiciona colapso autom��tico dos menus ao clicar em um item - Cria tabela interests no banco de dados com pol��ticas RLS --- CHANGELOG.md | 92 +---- src/components/ui/toast.tsx | 127 +++++++ src/components/ui/toaster.tsx | 33 ++ src/hooks/useToast.ts | 191 +++++++++++ src/main.tsx | 20 +- src/pages/dashboard/DashboardLayout.tsx | 275 +++++++++------ .../StudentDashboardLayout.tsx | 268 +++++++++------ .../student-dashboard/StudentSettingsPage.tsx | 316 ++++++++++++++++++ src/types/database.ts | 85 +++++ .../migrations/20240111_create_interests.sql | 51 +++ 10 files changed, 1170 insertions(+), 288 deletions(-) create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/hooks/useToast.ts create mode 100644 supabase/migrations/20240111_create_interests.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf8bb3..ca785e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,85 +1,23 @@ # Changelog -## [1.3.0] - 2024-03-21 +Todas as mudanças notáveis neste projeto serão documentadas neste arquivo. + +O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/), +e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). + +## [1.1.0] - 2024-03-19 ### Adicionado -- Implementada funcionalidade de deleção de histórias com confirmação -- Adicionado modal de confirmação para exclusão -- Integrado sistema de limpeza automática de recursos - -### Técnico -- Implementada lógica de deleção em cascata para recursos relacionados -- Criado fluxo otimizado para remoção de arquivos no storage -- Adicionado tratamento de erros robusto para o processo de deleção +- Nova aba "Interesses" nas configurações do aluno para capturar preferências +- Sistema de notificações toast usando Radix UI +- Tabela `interests` no banco de dados para armazenar interesses dos alunos ### Modificado -- Melhorada interface de gerenciamento de histórias -- Adicionado feedback visual durante processo de exclusão -- Implementada navegação automática após deleção bem-sucedida - -## [1.2.0] - 2024-12-31 - -### Adicionado - -- Implementado componente `WordHighlighter` para destacar palavras importantes durante a leitura -- Adicionado modal de detalhes da palavra com significado, sílabas e exemplos -- Integrado sistema de tracking de palavras difíceis por aluno +- Menu lateral do dashboard agora é responsivo e colapsável +- Menu lateral do dashboard do aluno agora é responsivo e colapsável +- Menus laterais agora colapsam automaticamente ao clicar em um item ### Técnico - -- Criado sistema de teste para o componente `WordHighlighter` -- Implementada integração com Jest/Vitest para testes de componentes -- Adicionado suporte a ARIA labels para acessibilidade -- Configurado ambiente de teste com setup adequado - -### Modificado - -- Melhorada a experiência de leitura com destaque visual de palavras importantes -- Implementado feedback visual para palavras difíceis e importantes -- Adicionado suporte a interatividade nas palavras destacadas - -## [1.2.1] - 2024-03-21 - -### Técnico -- Corrigida ordem de deleção de histórias para evitar problemas de integridade -- Otimizado processo de limpeza de arquivos no storage -- Melhorado tratamento de erros na deleção de histórias - -### Modificado -- Aprimorado fluxo de exclusão de histórias para garantir remoção completa de recursos -- Adicionada confirmação visual durante processo de deleção - -## [1.4.0] - 2024-03-21 - -### Adicionado -- Implementado sistema de exercícios de alfabetização -- Adicionados três tipos de exercícios: formação de palavras, completar frases e treino de pronúncia -- Criada página dedicada para exercícios -- Implementado tracking de progresso nos exercícios - -### Técnico -- Criada nova rota para exercícios -- Adicionada tabela exercise_progress -- Implementada lógica de navegação entre exercícios - -## [Não Publicado] - -### Modificado -- Melhorada a interface do exercício de Formação de Palavras - - Adicionada barra de progresso com animação - - Adicionado feedback visual para acertos e erros - - Adicionada lista de palavras encontradas - - Melhorada a interatividade dos botões de sílabas - - Adicionados estados visuais para feedback do usuário - - Melhorada a área de exibição da palavra atual - - Adicionado contador de progresso - - Melhorada a consistência visual com outros exercícios - -### Técnico -- Refatorado componente WordFormation para melhor gerenciamento de estado -- Adicionada validação para palavras repetidas -- Implementado sistema de feedback temporário -- Otimizadas transições e animações -- Corrigidos erros de tipagem no ExercisePage - - Adicionadas interfaces para Story e StoryRecording - - Melhorada type safety no acesso aos dados +- Implementação do hook `useToast` para gerenciamento de notificações +- Correção dos caminhos de importação para compatibilidade com Vite +- Adição de políticas de segurança RLS na tabela `interests` diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..1dc8a13 --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" +import { cn } from "../../lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-gray-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-white text-gray-900", + destructive: + "destructive group border-red-500 bg-red-500 text-white", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + console.log('Toast component props:', { className, variant, ...props }) + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..3da98b6 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "./toast" +import { useToast } from "../../hooks/useToast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, open, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} \ No newline at end of file diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 0000000..d9c23f6 --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,191 @@ +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "../components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 3000 // 3 segundos + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index fa6e277..62214dd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { router } from './routes'; +import { Toaster } from './components/ui/toaster'; import './index.css'; const queryClient = new QueryClient({ @@ -15,10 +16,15 @@ const queryClient = new QueryClient({ }, }); -createRoot(document.getElementById('root')!).render( - - - - - -); \ No newline at end of file +function Root() { + return ( + + + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); \ No newline at end of file diff --git a/src/pages/dashboard/DashboardLayout.tsx b/src/pages/dashboard/DashboardLayout.tsx index 850a589..b1d2eda 100644 --- a/src/pages/dashboard/DashboardLayout.tsx +++ b/src/pages/dashboard/DashboardLayout.tsx @@ -8,126 +8,205 @@ import { Settings, LogOut, School, - UserRound + UserRound, + Menu, + X, + ChevronLeft, + ChevronRight } from 'lucide-react'; import { useAuth } from '../../hooks/useAuth'; +import * as Dialog from '@radix-ui/react-dialog'; export function DashboardLayout() { const navigate = useNavigate(); const { signOut } = useAuth(); + const [isCollapsed, setIsCollapsed] = React.useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); const handleLogout = async () => { await signOut(); navigate('/'); }; + const handleNavigation = () => { + setIsMobileMenuOpen(false); + }; + + const NavItems = () => ( + + ); + return (
- {/* Sidebar */} -
diff --git a/src/pages/student-dashboard/StudentDashboardLayout.tsx b/src/pages/student-dashboard/StudentDashboardLayout.tsx index d7edae3..63218a1 100644 --- a/src/pages/student-dashboard/StudentDashboardLayout.tsx +++ b/src/pages/student-dashboard/StudentDashboardLayout.tsx @@ -7,134 +7,190 @@ import { LogOut, School, Trophy, - History + History, + Menu, + X, + ChevronLeft, + ChevronRight } from 'lucide-react'; import { useAuth } from '../../hooks/useAuth'; +import * as Dialog from '@radix-ui/react-dialog'; export function StudentDashboardLayout() { const navigate = useNavigate(); const { signOut } = useAuth(); + const [isCollapsed, setIsCollapsed] = React.useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); const handleLogout = async () => { await signOut(); navigate('/'); }; + const handleNavigation = () => { + setIsMobileMenuOpen(false); + }; + + const NavItems = () => ( + + ); + return (
- {/* Sidebar */} -
diff --git a/src/pages/student-dashboard/StudentSettingsPage.tsx b/src/pages/student-dashboard/StudentSettingsPage.tsx index d7224c9..59462b1 100644 --- a/src/pages/student-dashboard/StudentSettingsPage.tsx +++ b/src/pages/student-dashboard/StudentSettingsPage.tsx @@ -3,8 +3,246 @@ import { Input } from '../../components/ui/input'; import { DatePicker } from '../../components/ui/date-picker'; import { Select } from '../../components/ui/select'; import { AvatarUpload } from '../../components/ui/avatar-upload'; +import React from 'react'; +import { Heart, Gamepad2, Dog, Map, Pizza, School as SchoolIcon, Tv, Music, Palette, Trophy, Loader2 } from 'lucide-react'; +import { supabase } from '../../lib/supabase'; +import { useToast } from '../../hooks/useToast'; + +interface InterestState { + category: string; + items: string[]; +} export function StudentSettingsPage() { + const { toast } = useToast(); + const [interests, setInterests] = React.useState([ + { category: 'pets', items: [] }, + { category: 'entertainment', items: [] }, + { category: 'hobbies', items: [] }, + { category: 'places', items: [] }, + { category: 'food', items: [] }, + { category: 'school', items: [] }, + { category: 'shows', items: [] }, + { category: 'music', items: [] }, + { category: 'art', items: [] }, + { category: 'dreams', items: [] } + ]); + const [loading, setLoading] = React.useState(false); + const [saving, setSaving] = React.useState(false); + + React.useEffect(() => { + const loadInterests = async () => { + try { + setLoading(true); + const { data: { session } } = await supabase.auth.getSession(); + if (!session?.user?.id) return; + + const { data, error } = await supabase + .from('interests') + .select('*') + .eq('student_id', session.user.id); + + if (error) throw error; + + if (data) { + // Agrupa os interesses por categoria + const groupedInterests = data.reduce((acc, interest) => { + const category = interest.category; + if (!acc[category]) { + acc[category] = []; + } + if (interest.item) { + acc[category].push(interest.item); + } + return acc; + }, {} as Record); + + // Atualiza o estado com os interesses agrupados + const loadedInterests = interests.map(interest => ({ + ...interest, + items: groupedInterests[interest.category] || [] + })); + setInterests(loadedInterests); + } + } catch (error) { + console.error('Erro ao carregar interesses:', error); + toast({ + title: 'Erro', + description: 'Não foi possível carregar seus interesses', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + loadInterests(); + }, []); + + const handleInterestChange = async (category: string, value: string) => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session?.user?.id) { + toast({ + title: 'Erro', + description: 'Você precisa estar logado para adicionar interesses', + variant: 'destructive' + }); + return; + } + + // Adiciona o novo interesse no banco de dados + const { error } = await supabase + .from('interests') + .insert({ + student_id: session.user.id, + category: category, + item: value, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }); + + if (error) throw error; + + // Atualiza o estado local + setInterests(prev => + prev.map(interest => + interest.category === category + ? { ...interest, items: [...interest.items, value] } + : interest + ) + ); + + toast({ + title: 'Sucesso', + description: 'Interesse adicionado com sucesso', + }); + } catch (error) { + console.error('Erro ao adicionar interesse:', error); + toast({ + title: 'Erro', + description: 'Não foi possível adicionar o interesse', + variant: 'destructive' + }); + } + }; + + const handleRemoveInterest = async (category: string, item: string) => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session?.user?.id) { + toast({ + title: 'Erro', + description: 'Você precisa estar logado para remover interesses', + variant: 'destructive' + }); + return; + } + + // Remove o interesse do banco de dados + const { error } = await supabase + .from('interests') + .delete() + .match({ + student_id: session.user.id, + category: category, + item: item + }); + + if (error) throw error; + + // Atualiza o estado local + setInterests(prev => + prev.map(interest => + interest.category === category + ? { ...interest, items: interest.items.filter(i => i !== item) } + : interest + ) + ); + + toast({ + title: 'Sucesso', + description: 'Interesse removido com sucesso', + }); + } catch (error) { + console.error('Erro ao remover interesse:', error); + toast({ + title: 'Erro', + description: 'Não foi possível remover o interesse', + variant: 'destructive' + }); + } + }; + + const InterestInput = ({ category, icon, placeholder }: { category: string; icon: React.ReactNode; placeholder: string }) => { + const [inputValue, setInputValue] = React.useState(''); + const [isAdding, setIsAdding] = React.useState(false); + + const handleAdd = async () => { + const value = inputValue.trim(); + if (value && !isAdding) { + setIsAdding(true); + await handleInterestChange(category, value); + setInputValue(''); + setIsAdding(false); + } + }; + + return ( +
+
+ {icon} + {category} +
+
+
+ setInputValue(e.target.value)} + placeholder={placeholder} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAdd(); + } + }} + /> + +
+
+ {interests.find(i => i.category === category)?.items.map((item) => ( + + {item} + + + ))} +
+
+
+ ); + }; + return (

@@ -14,6 +252,7 @@ export function StudentSettingsPage() { Informações Pessoais + Interesses Preferências Acessibilidade Notificações @@ -60,6 +299,83 @@ export function StudentSettingsPage() {

+ + +
+ {loading ? ( +
+ +
+ ) : ( +
+
+ } + placeholder="Adicione seus pets ou animais favoritos..." + /> + } + placeholder="Adicione seus jogos e diversões favoritos..." + /> + } + placeholder="Adicione seus hobbies e atividades..." + /> + } + placeholder="Adicione lugares que você gosta..." + /> + } + placeholder="Adicione suas comidas favoritas..." + /> + } + placeholder="Adicione suas matérias favoritas..." + /> + } + placeholder="Adicione seus programas e séries favoritos..." + /> + } + placeholder="Adicione suas músicas e artistas favoritos..." + /> + } + placeholder="Adicione suas artes e criações favoritas..." + /> + } + placeholder="Adicione seus sonhos e conquistas..." + /> +
+
+ )} +
+
+ + + {/* Conteúdo da tab de preferências */} + + + + {/* Conteúdo da tab de acessibilidade */} + + + + {/* Conteúdo da tab de notificações */} + ); diff --git a/src/types/database.ts b/src/types/database.ts index 96f7646..003b178 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -147,4 +147,89 @@ export interface StoryRecording { suggestions: string; created_at: string; processed_at: string | null; +} + +export interface Interest { + id: string; + student_id: string; + category: string; + items: string[]; + created_at: string; + updated_at: string; +} + +export interface Student { + id: string; + name: string; + email: string; + avatar_url?: string; + class_id: string; + school_id: string; + status: 'active' | 'inactive'; + created_at: string; + updated_at: string; + interests?: Interest[]; + class?: { + id: string; + name: string; + grade: string; + }; + school?: { + id: string; + name: string; + }; +} + +export interface Story { + id: string; + title: string; + content: string; + student_id: string; + status: 'draft' | 'published'; + created_at: string; + updated_at: string; + cover?: { + id: string; + image_url: string; + }; + students?: { + name: string; + }; +} + +export interface Class { + id: string; + name: string; + grade: string; + school_id: string; + created_at: string; + updated_at: string; + students?: { + count: number; + }; +} + +export interface School { + id: string; + name: string; + email: string; + phone?: string; + address?: string; + city?: string; + state?: string; + zip_code?: string; + director_name?: string; + created_at: string; + updated_at: string; +} + +export interface SchoolSettings { + name: string; + email: string; + phone: string; + address: string; + city: string; + state: string; + zipCode: string; + directorName: string; } \ No newline at end of file diff --git a/supabase/migrations/20240111_create_interests.sql b/supabase/migrations/20240111_create_interests.sql new file mode 100644 index 0000000..24dd673 --- /dev/null +++ b/supabase/migrations/20240111_create_interests.sql @@ -0,0 +1,51 @@ +-- Create the interests table +CREATE TABLE IF NOT EXISTS interests ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + student_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + category TEXT NOT NULL, + item TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, + UNIQUE(student_id, category, item) +); + +-- Enable Row Level Security +ALTER TABLE interests ENABLE ROW LEVEL SECURITY; + +-- Create policies +CREATE POLICY "Students can view their own interests" + ON interests FOR SELECT + USING (auth.uid() = student_id); + +CREATE POLICY "Students can insert their own interests" + ON interests FOR INSERT + WITH CHECK (auth.uid() = student_id); + +CREATE POLICY "Students can update their own interests" + ON interests FOR UPDATE + USING (auth.uid() = student_id) + WITH CHECK (auth.uid() = student_id); + +CREATE POLICY "Students can delete their own interests" + ON interests FOR DELETE + USING (auth.uid() = student_id); + +-- Create indexes +CREATE INDEX interests_student_id_idx ON interests(student_id); +CREATE INDEX interests_category_idx ON interests(category); +CREATE INDEX interests_item_idx ON interests(item); + +-- Create function to automatically update updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = timezone('utc'::text, now()); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create trigger to automatically update updated_at +CREATE TRIGGER update_interests_updated_at + BEFORE UPDATE ON interests + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file