Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion frontend-vite/react-ts/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ function PageViewTracker() {
return null;
}

function DashboardPage() {
return (
<div className="flex flex-col md:flex-row min-h-screen bg-background">
<Sidebar />
<main className="flex-1 min-w-0">
<Dashboard />
</main>
</div>
);
}

function UploadPage() {
return (
<div className="flex flex-col md:flex-row min-h-screen bg-background">
Expand Down Expand Up @@ -55,7 +66,7 @@ function App() {
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
<DashboardPage />
</ProtectedRoute>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BARCODE_COLUMN_HELP, TEXT_FIELDS_HELP, HEADER_ROW_HELP } from './column
import type { HelpContent } from './columnMappingHelp';

const TOUR_KEY = 'lgu_col_map_tour_v1';
const ALLOWED_ROUTES = ['/dashboard', '/upload'];
const ALLOWED_ROUTES = ['/upload'];

const steps: HelpContent[] = [
{
Expand Down
21 changes: 3 additions & 18 deletions frontend-vite/react-ts/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import axios from 'axios';
import {
ScanBarcode,
LayoutDashboard,
Upload,
Plus,
Package,
HelpCircle,
LogOut,
Expand All @@ -20,26 +20,11 @@ interface SidebarProps {
function Sidebar({ activeOverride }: SidebarProps) {
const location = useLocation();
const navigate = useNavigate();
const [userEmail, setUserEmail] = useState('');
const [mobileOpen, setMobileOpen] = useState(false);
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

const currentPath = activeOverride || location.pathname;

useEffect(() => {
const fetchUser = async () => {
try {
const res = await axios.get(`${API_URL}/auth/status`, { withCredentials: true });
if (res.data.authenticated && res.data.email) {
setUserEmail(res.data.email);
}
} catch {
// ignore
}
};
fetchUser();
}, []);

// Close drawer on route change
useEffect(() => {
setMobileOpen(false);
Expand All @@ -66,7 +51,7 @@ function Sidebar({ activeOverride }: SidebarProps) {

const navItems = [
{ label: 'Dashboard', icon: LayoutDashboard, path: '/dashboard' },
{ label: 'Upload Labels', icon: Upload, path: '/upload' },
{ label: 'Create Labels', icon: Plus, path: '/upload' },
{ label: 'Inventory', icon: Package, path: '/inventory' },
{ label: 'Help', icon: HelpCircle, path: '/help' },
];
Expand Down Expand Up @@ -105,7 +90,7 @@ function Sidebar({ activeOverride }: SidebarProps) {
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{userEmail || 'User'}
User
</p>
</div>
<button
Expand Down
202 changes: 195 additions & 7 deletions frontend-vite/react-ts/src/components/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,201 @@
import Sidebar from './Sidebar';
import LabelUploader from './labelUploader';
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { FileSpreadsheet, Download, Trash2, ArrowRight, AlertCircle } from 'lucide-react';

interface UserSheet {
id: string;
original_filename: string;
label_count: number;
sheet_count: number;
created_at: string;
}

function Dashboard() {
const navigate = useNavigate();
const [sheets, setSheets] = useState<UserSheet[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [deletingId, setDeletingId] = useState<string | null>(null);

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

useEffect(() => {
const fetchData = async () => {
try {
const sheetsRes = await axios.get(`${API_URL}/my-sheets`, { withCredentials: true });
setSheets(sheetsRes.data.sheets || []);
} catch {
setError('Failed to load your sheets.');
} finally {
setLoading(false);
}
};
fetchData();
}, []);

const totalLabels = useMemo(() => sheets.reduce((sum, s) => sum + s.label_count, 0), [sheets]);
const recentSheets = useMemo(
() =>
[...sheets]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 5),
[sheets]
);

const handleDownload = async (sheetId: string, filename: string) => {
try {
const response = await axios.get(`${API_URL}/download-sheet/${sheetId}`, {
withCredentials: true,
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${filename}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch {
setError('Failed to download sheet.');
setTimeout(() => setError(''), 5000);
}
};

const handleDelete = async (sheetId: string) => {
if (!window.confirm('Delete this sheet? This cannot be undone.')) return;
setDeletingId(sheetId);
try {
await axios.delete(`${API_URL}/delete-sheet/${sheetId}`, { withCredentials: true });
setSheets(prev => prev.filter(s => s.id !== sheetId));
} catch {
setError('Failed to delete sheet.');
setTimeout(() => setError(''), 5000);
} finally {
setDeletingId(null);
}
};

const formatDate = (dateStr: string) =>
new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });

const h = new Date().getHours();
const greeting = h < 12 ? 'Good morning' : h < 17 ? 'Good afternoon' : 'Good evening';

return (
<div className="flex flex-col md:flex-row min-h-screen bg-background">
<Sidebar />
<main className="flex-1 min-w-0">
<LabelUploader />
</main>
<div className="p-8 md:p-10 max-w-5xl">
{/* Welcome Header */}
<div className="flex items-center justify-between mb-8">
<div>
<p className="text-sm text-muted-foreground">{greeting},</p>
<h1 className="font-heading text-2xl font-bold text-foreground mt-0.5">Welcome back</h1>
</div>
<button
onClick={() => navigate('/upload')}
className="flex items-center gap-2 h-11 px-6 bg-primary text-primary-foreground font-heading text-sm font-medium rounded-full hover:bg-primary/90 transition-colors cursor-pointer border-none"
>
Create Labels <ArrowRight className="w-4 h-4" />
</button>
</div>

{/* Stats */}
<div className="flex gap-4 mb-8">
<div className="bg-card border border-border rounded-[16px] px-6 py-5 min-w-[180px]">
<p className="text-3xl font-bold text-foreground font-heading">{loading ? '—' : sheets.length}</p>
<p className="text-sm text-muted-foreground mt-1">Sheets Generated</p>
</div>
<div className="bg-card border border-border rounded-[16px] px-6 py-5 min-w-[180px]">
<p className="text-3xl font-bold text-primary font-heading">{loading ? '—' : totalLabels.toLocaleString()}</p>
<p className="text-sm text-muted-foreground mt-1">Total Labels</p>
</div>
</div>

{error && (
<div className="mb-6 flex items-start gap-3 p-3 bg-error rounded-[12px]">
<AlertCircle className="w-4 h-4 text-error-foreground mt-0.5 flex-shrink-0" />
<p className="text-sm text-error-foreground">{error}</p>
</div>
)}

{/* Recent Sheets */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="font-heading text-base font-semibold text-foreground">Recent Sheets</h2>
{sheets.length > 0 && (
<button
onClick={() => navigate('/inventory')}
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors bg-transparent border-none cursor-pointer"
>
View All →
</button>
)}
</div>

<div className="bg-card border border-border rounded-[16px] overflow-hidden">
{loading ? (
<div className="px-6 py-12 text-center text-sm text-muted-foreground">Loading...</div>
) : recentSheets.length === 0 ? (
<div className="px-6 py-12 text-center">
<p className="text-sm text-muted-foreground mb-3">No sheets yet.</p>
<button
onClick={() => navigate('/upload')}
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors bg-transparent border-none cursor-pointer"
>
Create your first labels →
</button>
</div>
) : (
<table className="w-full">
<thead>
<tr className="bg-secondary/50 border-b border-border">
<th className="text-left px-5 py-3 text-xs font-semibold text-muted-foreground">File</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground w-28">Labels</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground w-24">Sheets</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground w-36">Created</th>
<th className="px-4 py-3 w-24"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{recentSheets.map((sheet) => (
<tr key={sheet.id} className="hover:bg-secondary/30 transition-colors">
<td className="px-5 py-4">
<div className="flex items-center gap-2.5">
<FileSpreadsheet className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="text-sm font-medium text-foreground truncate max-w-[280px]">
{sheet.original_filename}
</span>
</div>
</td>
<td className="px-4 py-4 text-sm text-foreground">{sheet.label_count}</td>
<td className="px-4 py-4 text-sm text-foreground">{sheet.sheet_count}</td>
<td className="px-4 py-4 text-sm text-muted-foreground">{formatDate(sheet.created_at)}</td>
<td className="px-4 py-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => handleDownload(sheet.id, sheet.original_filename)}
className="w-8 h-8 rounded-[8px] bg-secondary flex items-center justify-center hover:bg-border transition-colors cursor-pointer border-none"
title="Download"
>
<Download className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button
onClick={() => handleDelete(sheet.id)}
disabled={deletingId === sheet.id}
className="w-8 h-8 rounded-[8px] bg-secondary flex items-center justify-center hover:bg-error/40 transition-colors cursor-pointer border-none disabled:opacity-50"
title="Delete"
>
<Trash2 className="w-3.5 h-3.5 text-destructive" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
);
}
Expand Down
20 changes: 15 additions & 5 deletions frontend-vite/react-ts/src/components/labelUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState, useRef } from 'react';
import type { ChangeEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import {
Upload, AlertCircle, CheckCircle2, Plus, X,
Upload, AlertCircle, CheckCircle2, Plus, X, ArrowRight,
} from 'lucide-react';
import { usePostHog } from '@posthog/react';
import LabelPreview from './LabelPreview';
Expand Down Expand Up @@ -30,6 +31,7 @@ interface PreviewData {
const MAX_FILE_SIZE = 50 * 1024 * 1024

function LabelUploader() {
const navigate = useNavigate();
const posthog = usePostHog();
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
Expand Down Expand Up @@ -128,7 +130,7 @@ function LabelUploader() {
});
setFile(null); setPreviewData(null);
if (fileInputRef.current) fileInputRef.current.value = '';
setTimeout(() => { setSuccess(''); setUploadProgress(0); setProcessingStatus(''); }, 5000);
setUploadProgress(0); setProcessingStatus('');
} catch (err) {
setUploadProgress(0); setProcessingStatus('');
if (axios.isAxiosError(err)) {
Expand Down Expand Up @@ -237,9 +239,17 @@ function LabelUploader() {
)}

{success && (
<div className="mt-4 flex items-start gap-3 p-3 bg-success rounded-[12px]">
<CheckCircle2 className="w-4 h-4 text-success-foreground mt-0.5 flex-shrink-0" />
<p className="text-sm text-success-foreground">{success}</p>
<div className="mt-4 flex items-start justify-between gap-3 p-3 bg-success rounded-[12px]">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-4 h-4 text-success-foreground mt-0.5 flex-shrink-0" />
<p className="text-sm text-success-foreground">{success}</p>
</div>
<button
onClick={() => navigate('/inventory')}
className="flex-shrink-0 flex items-center gap-1.5 text-sm font-medium text-success-foreground hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer whitespace-nowrap"
>
View in Inventory <ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
Expand Down
Loading