SOURCE.IO / src /components /AppSidebar.tsx
Adeen
Deploy: OCR support for PDF/Images/DOCX
ae14296
import { useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/useAuth";
import { useWorkspace, DocumentRow } from "@/store/workspace";
import { Button } from "@/components/ui/button";
import { Sparkles, Plus, FileText, Mic, Video, Youtube, FileType2, LogOut, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { useToast } from "@/hooks/use-toast";
const sourceIcon = {
pdf: FileType2,
docx: FileType2,
text: FileText,
audio: Mic,
video: Video,
youtube: Youtube,
} as const;
export default function AppSidebar({ onNew, onNavigate }: { onNew: () => void; onNavigate?: () => void }) {
const { user, signOut } = useAuth();
const { documents, setDocuments, upsertDocument, removeDocument } = useWorkspace();
const { docId } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
useEffect(() => {
if (!user) return;
let mounted = true;
(async () => {
const { data, error } = await supabase
.from("documents")
.select("id,title,source_type,status,error_code,created_at")
.order("created_at", { ascending: false });
if (error) {
toast({ title: "Failed to load documents", description: error.message, variant: "destructive" });
return;
}
if (mounted && data) setDocuments(data as DocumentRow[]);
})();
const channel = supabase
.channel("documents-sidebar")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "documents", filter: `user_id=eq.${user.id}` },
(payload) => {
if (payload.eventType === "DELETE") {
removeDocument((payload.old as any).id);
} else {
const row = payload.new as any;
upsertDocument({
id: row.id,
title: row.title,
source_type: row.source_type,
status: row.status,
error_code: row.error_code,
created_at: row.created_at,
});
}
}
)
.subscribe();
return () => {
mounted = false;
supabase.removeChannel(channel);
};
}, [user, setDocuments, upsertDocument, removeDocument, toast]);
return (
<aside className="w-64 shrink-0 border-r border-sidebar-border bg-sidebar flex flex-col h-screen md:w-64 w-[18rem]">
<div className="p-3 border-b border-sidebar-border">
<Link to="/app" className="flex items-center gap-2 px-2 py-1.5">
<div className="h-7 w-7 rounded-md bg-gradient-primary flex items-center justify-center shadow-glow">
<Sparkles className="h-4 w-4 text-primary-foreground" />
</div>
<span className="font-semibold tracking-tight">Source.AI</span>
</Link>
</div>
<div className="p-3">
<Button onClick={onNew} className="w-full justify-start" size="sm">
<Plus className="h-4 w-4 mr-2" /> New document
</Button>
</div>
<div className="flex-1 overflow-y-auto px-2 pb-2">
<div className="text-[11px] uppercase tracking-wide text-muted-foreground px-2 py-2">Library</div>
{documents.length === 0 && (
<div className="text-xs text-muted-foreground px-2 py-4 text-center">No documents yet.</div>
)}
<ul className="space-y-0.5">
{documents.map((d) => {
const Icon = sourceIcon[d.source_type] ?? FileText;
const active = d.id === docId;
return (
<li key={d.id}>
<button
onClick={() => {
navigate(`/app/doc/${d.id}`);
onNavigate?.();
}}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm text-left transition-colors",
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "hover:bg-sidebar-accent/60 text-sidebar-foreground"
)}
>
<Icon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate flex-1">{d.title}</span>
{d.status !== "ready" && d.status !== "failed" && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
)}
{d.status === "failed" && (
<span className="h-1.5 w-1.5 rounded-full bg-destructive" />
)}
</button>
</li>
);
})}
</ul>
</div>
<div className="p-2 border-t border-sidebar-border">
<div className="flex items-center gap-2 px-2 py-1.5">
<div className="h-7 w-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium">
{(user?.email ?? "?").slice(0, 1).toUpperCase()}
</div>
<div className="flex-1 truncate text-sm">{user?.email}</div>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={signOut} title="Sign out">
<LogOut className="h-4 w-4" />
</Button>
</div>
</div>
</aside>
);
}