biome lint applied

This commit is contained in:
BeauTroll
2026-01-19 18:09:23 +01:00
parent e8269bb2fe
commit cbd0cdced4
30 changed files with 1973 additions and 1971 deletions

View File

@@ -1,37 +1,37 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"includes": ["src/**/*.ts", "src/**/*.tsx"], "includes": ["src/**/*.ts", "src/**/*.tsx"],
"ignoreUnknown": false "ignoreUnknown": false
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "tab" "indentStyle": "tab"
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true
} }
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "single", "quoteStyle": "single",
"semicolons": "always", "semicolons": "always",
"trailingCommas": "all" "trailingCommas": "all"
} }
}, },
"assist": { "assist": {
"enabled": true, "enabled": true,
"actions": { "actions": {
"source": { "source": {
"organizeImports": "on" "organizeImports": "on"
} }
} }
} }
} }

View File

@@ -57,8 +57,8 @@ Draft
- [ ] Task 5: Configure linting and formatting (AC: 5) - [ ] Task 5: Configure linting and formatting (AC: 5)
- [x] Install Biome (as specified in tech stack, not ESLint+Prettier) - [x] Install Biome (as specified in tech stack, not ESLint+Prettier)
- [ ] Create `biome.json` with recommended rules - [x] Create `biome.json` with recommended rules
- [ ] Add lint and format scripts to `package.json` - [x] Add lint and format scripts to `package.json`
- [ ] Verify linting works on project files - [ ] Verify linting works on project files
- [ ] Task 6: Setup Gitea Actions workflow (AC: 6) - [ ] Task 6: Setup Gitea Actions workflow (AC: 6)

View File

@@ -1,27 +1,27 @@
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router';
import './Header.css' import './Header.css';
export default function Header() { export default function Header() {
return ( return (
<header className="header"> <header className="header">
<nav className="nav"> <nav className="nav">
<div className="nav-item"> <div className="nav-item">
<Link to="/">Home</Link> <Link to="/">Home</Link>
</div> </div>
<div className="px-2 font-bold"> <div className="px-2 font-bold">
<Link to="/demo/start/server-funcs">Start - Server Functions</Link> <Link to="/demo/start/server-funcs">Start - Server Functions</Link>
</div> </div>
<div className="px-2 font-bold"> <div className="px-2 font-bold">
<Link to="/demo/start/api-request">Start - API Request</Link> <Link to="/demo/start/api-request">Start - API Request</Link>
</div> </div>
<div className="px-2 font-bold"> <div className="px-2 font-bold">
<Link to="/demo/start/ssr">Start - SSR Demos</Link> <Link to="/demo/start/ssr">Start - SSR Demos</Link>
</div> </div>
</nav> </nav>
</header> </header>
) );
} }

View File

@@ -1,437 +1,437 @@
import * as React from "react";
import { import {
Search, ArrowUpDown,
Plus, ChevronDown,
ChevronDown, ChevronUp,
ChevronUp, Plus,
ArrowUpDown, Search,
} from "lucide-react"; } from 'lucide-react';
import { Input } from "@/components/ui/input"; import * as React from 'react';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from '@/components/ui/checkbox';
import { import {
Table, Collapsible,
TableBody, CollapsibleContent,
TableCell, CollapsibleTrigger,
TableHead, } from '@/components/ui/collapsible';
TableHeader,
TableRow,
} from "@/components/ui/table";
import { import {
Collapsible, DropdownMenu,
CollapsibleContent, DropdownMenuContent,
CollapsibleTrigger, DropdownMenuItem,
} from "@/components/ui/collapsible"; DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from '@/components/ui/select';
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { import {
DropdownMenu, Table,
DropdownMenuContent, TableBody,
DropdownMenuItem, TableCell,
DropdownMenuTrigger, TableHead,
} from "@/components/ui/dropdown-menu"; TableHeader,
import { cn } from "@/lib/utils"; TableRow,
} from '@/components/ui/table';
import { cn } from '@/lib/utils';
// Sample data // Sample data
const characters = [ const characters = [
{ {
id: 1, id: 1,
nom: "Krosmaster", nom: 'Krosmaster',
classe: "Cra", classe: 'Cra',
niveau: 200, niveau: 200,
serveur: "Imagiro", serveur: 'Imagiro',
compte: "Compte1", compte: 'Compte1',
}, },
{ {
id: 2, id: 2,
nom: "TankMaster", nom: 'TankMaster',
classe: "Iop", classe: 'Iop',
niveau: 200, niveau: 200,
serveur: "Imagiro", serveur: 'Imagiro',
compte: "Compte1", compte: 'Compte1',
}, },
{ {
id: 3, id: 3,
nom: "MoneyMaker", nom: 'MoneyMaker',
classe: "Enu", classe: 'Enu',
niveau: 200, niveau: 200,
serveur: "Imagiro", serveur: 'Imagiro',
compte: "Compte2", compte: 'Compte2',
}, },
{ {
id: 4, id: 4,
nom: "HealBot", nom: 'HealBot',
classe: "Eni", classe: 'Eni',
niveau: 195, niveau: 195,
serveur: "Tylezia", serveur: 'Tylezia',
compte: "Compte2", compte: 'Compte2',
}, },
{ {
id: 5, id: 5,
nom: "ShadowKill", nom: 'ShadowKill',
classe: "Sram", classe: 'Sram',
niveau: 200, niveau: 200,
serveur: "Draconiros", serveur: 'Draconiros',
compte: "Compte3", compte: 'Compte3',
}, },
{ {
id: 6, id: 6,
nom: "TimeWarp", nom: 'TimeWarp',
classe: "Elio", classe: 'Elio',
niveau: 180, niveau: 180,
serveur: "Imagiro", serveur: 'Imagiro',
compte: "Compte1", compte: 'Compte1',
}, },
{ {
id: 7, id: 7,
nom: "ArrowStorm", nom: 'ArrowStorm',
classe: "Cra", classe: 'Cra',
niveau: 200, niveau: 200,
serveur: "Tylezia", serveur: 'Tylezia',
compte: "Compte4", compte: 'Compte4',
}, },
{ {
id: 8, id: 8,
nom: "BerserkerX", nom: 'BerserkerX',
classe: "Iop", classe: 'Iop',
niveau: 175, niveau: 175,
serveur: "Draconiros", serveur: 'Draconiros',
compte: "Compte3", compte: 'Compte3',
}, },
]; ];
const classes = [ const classes = [
{ name: "Cra", count: 12 }, { name: 'Cra', count: 12 },
{ name: "Iop", count: 8 }, { name: 'Iop', count: 8 },
{ name: "Enu", count: 6 }, { name: 'Enu', count: 6 },
{ name: "Eni", count: 5 }, { name: 'Eni', count: 5 },
{ name: "Elio", count: 4 }, { name: 'Elio', count: 4 },
{ name: "Sram", count: 6 }, { name: 'Sram', count: 6 },
]; ];
const serveurs = ["Imagiro", "Tylezia", "Draconiros"]; const serveurs = ['Imagiro', 'Tylezia', 'Draconiros'];
export function CharacterList() { export function CharacterList() {
const [selectedIds, setSelectedIds] = React.useState<number[]>([]); const [selectedIds, setSelectedIds] = React.useState<number[]>([]);
const [classeOpen, setClasseOpen] = React.useState(true); const [classeOpen, setClasseOpen] = React.useState(true);
const [serveurOpen, setServeurOpen] = React.useState(true); const [serveurOpen, setServeurOpen] = React.useState(true);
const [progressionOpen, setProgressionOpen] = React.useState(true); const [progressionOpen, setProgressionOpen] = React.useState(true);
const toggleSelect = (id: number) => { const toggleSelect = (id: number) => {
setSelectedIds((prev) => setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id], prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
); );
}; };
const toggleSelectAll = () => { const toggleSelectAll = () => {
if (selectedIds.length === characters.length) { if (selectedIds.length === characters.length) {
setSelectedIds([]); setSelectedIds([]);
} else { } else {
setSelectedIds(characters.map((c) => c.id)); setSelectedIds(characters.map((c) => c.id));
} }
}; };
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
{/* Filter Sidebar */} {/* Filter Sidebar */}
<aside className="w-[250px] border-r border-[#334155] bg-[#1E293B] p-4 sticky top-0 h-screen overflow-y-auto"> <aside className="w-[250px] border-r border-[#334155] bg-[#1E293B] p-4 sticky top-0 h-screen overflow-y-auto">
<h2 className="text-[#F8FAFC] font-semibold text-lg mb-6">Filtres</h2> <h2 className="text-[#F8FAFC] font-semibold text-lg mb-6">Filtres</h2>
{/* Classe Section */} {/* Classe Section */}
<Collapsible open={classeOpen} onOpenChange={setClasseOpen}> <Collapsible open={classeOpen} onOpenChange={setClasseOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full mb-3"> <CollapsibleTrigger className="flex items-center justify-between w-full mb-3">
<span className="text-xs uppercase tracking-wider text-[#94A3B8]"> <span className="text-xs uppercase tracking-wider text-[#94A3B8]">
Classe Classe
</span> </span>
{classeOpen ? ( {classeOpen ? (
<ChevronUp className="h-4 w-4 text-[#94A3B8]" /> <ChevronUp className="h-4 w-4 text-[#94A3B8]" />
) : ( ) : (
<ChevronDown className="h-4 w-4 text-[#94A3B8]" /> <ChevronDown className="h-4 w-4 text-[#94A3B8]" />
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mb-6"> <CollapsibleContent className="space-y-2 mb-6">
{classes.map((classe) => ( {classes.map((classe) => (
<label <label
key={classe.name} key={classe.name}
className="flex items-center gap-3 cursor-pointer group" className="flex items-center gap-3 cursor-pointer group"
> >
<Checkbox className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" /> <Checkbox className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" />
<span className="text-[#F8FAFC] text-sm group-hover:text-[#60A5FA] transition-colors"> <span className="text-[#F8FAFC] text-sm group-hover:text-[#60A5FA] transition-colors">
{classe.name} {classe.name}
</span> </span>
<span className="text-[#64748B] text-sm ml-auto"> <span className="text-[#64748B] text-sm ml-auto">
({classe.count}) ({classe.count})
</span> </span>
</label> </label>
))} ))}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* Serveur Section */} {/* Serveur Section */}
<Collapsible open={serveurOpen} onOpenChange={setServeurOpen}> <Collapsible open={serveurOpen} onOpenChange={setServeurOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full mb-3"> <CollapsibleTrigger className="flex items-center justify-between w-full mb-3">
<span className="text-xs uppercase tracking-wider text-[#94A3B8]"> <span className="text-xs uppercase tracking-wider text-[#94A3B8]">
Serveur Serveur
</span> </span>
{serveurOpen ? ( {serveurOpen ? (
<ChevronUp className="h-4 w-4 text-[#94A3B8]" /> <ChevronUp className="h-4 w-4 text-[#94A3B8]" />
) : ( ) : (
<ChevronDown className="h-4 w-4 text-[#94A3B8]" /> <ChevronDown className="h-4 w-4 text-[#94A3B8]" />
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mb-6"> <CollapsibleContent className="space-y-2 mb-6">
{serveurs.map((serveur) => ( {serveurs.map((serveur) => (
<label <label
key={serveur} key={serveur}
className="flex items-center gap-3 cursor-pointer group" className="flex items-center gap-3 cursor-pointer group"
> >
<Checkbox className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" /> <Checkbox className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" />
<span className="text-[#F8FAFC] text-sm group-hover:text-[#60A5FA] transition-colors"> <span className="text-[#F8FAFC] text-sm group-hover:text-[#60A5FA] transition-colors">
{serveur} {serveur}
</span> </span>
</label> </label>
))} ))}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* Progression Section */} {/* Progression Section */}
<Collapsible open={progressionOpen} onOpenChange={setProgressionOpen}> <Collapsible open={progressionOpen} onOpenChange={setProgressionOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full mb-3"> <CollapsibleTrigger className="flex items-center justify-between w-full mb-3">
<span className="text-xs uppercase tracking-wider text-[#94A3B8]"> <span className="text-xs uppercase tracking-wider text-[#94A3B8]">
Progression Progression
</span> </span>
{progressionOpen ? ( {progressionOpen ? (
<ChevronUp className="h-4 w-4 text-[#94A3B8]" /> <ChevronUp className="h-4 w-4 text-[#94A3B8]" />
) : ( ) : (
<ChevronDown className="h-4 w-4 text-[#94A3B8]" /> <ChevronDown className="h-4 w-4 text-[#94A3B8]" />
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mb-6"> <CollapsibleContent className="space-y-4 mb-6">
<div> <div>
<label className="text-xs text-[#94A3B8] mb-2 block">Type</label> <label className="text-xs text-[#94A3B8] mb-2 block">Type</label>
<Select> <Select>
<SelectTrigger className="w-full bg-[#0F172A] border-[#475569] text-[#F8FAFC] h-9 rounded-[6px]"> <SelectTrigger className="w-full bg-[#0F172A] border-[#475569] text-[#F8FAFC] h-9 rounded-[6px]">
<SelectValue placeholder="Sélectionner..." /> <SelectValue placeholder="Sélectionner..." />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-[#1E293B] border-[#475569]"> <SelectContent className="bg-[#1E293B] border-[#475569]">
<SelectItem <SelectItem
value="dofus" value="dofus"
className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]" className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"
> >
Dofus Dofus
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value="donjons" value="donjons"
className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]" className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"
> >
Donjons Donjons
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value="recherches" value="recherches"
className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]" className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"
> >
Recherchés Recherchés
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<RadioGroup defaultValue="a-fait" className="gap-2"> <RadioGroup defaultValue="a-fait" className="gap-2">
<label className="flex items-center gap-3 cursor-pointer"> <label className="flex items-center gap-3 cursor-pointer">
<RadioGroupItem <RadioGroupItem
value="a-fait" value="a-fait"
className="border-[#475569] text-[#60A5FA]" className="border-[#475569] text-[#60A5FA]"
/> />
<span className="text-[#F8FAFC] text-sm">A fait</span> <span className="text-[#F8FAFC] text-sm">A fait</span>
</label> </label>
<label className="flex items-center gap-3 cursor-pointer"> <label className="flex items-center gap-3 cursor-pointer">
<RadioGroupItem <RadioGroupItem
value="na-pas-fait" value="na-pas-fait"
className="border-[#475569] text-[#60A5FA]" className="border-[#475569] text-[#60A5FA]"
/> />
<span className="text-[#F8FAFC] text-sm">{"N'a pas fait"}</span> <span className="text-[#F8FAFC] text-sm">{"N'a pas fait"}</span>
</label> </label>
</RadioGroup> </RadioGroup>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* Reset Button */} {/* Reset Button */}
<Button <Button
variant="outline" variant="outline"
className="w-full mt-4 border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="w-full mt-4 border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
Réinitialiser Réinitialiser
</Button> </Button>
</aside> </aside>
{/* Main Content */} {/* Main Content */}
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center justify-between p-4 border-b border-[#334155] bg-[#0F172A]"> <div className="flex items-center justify-between p-4 border-b border-[#334155] bg-[#0F172A]">
<div className="relative w-72"> <div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#64748B]" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#64748B]" />
<Input <Input
placeholder="Rechercher..." placeholder="Rechercher..."
className="pl-10 bg-[#1E293B] border-[#475569] text-[#F8FAFC] placeholder:text-[#64748B] h-9 rounded-[6px]" className="pl-10 bg-[#1E293B] border-[#475569] text-[#F8FAFC] placeholder:text-[#64748B] h-9 rounded-[6px]"
/> />
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<> <>
<span className="text-sm text-[#94A3B8]"> <span className="text-sm text-[#94A3B8]">
{selectedIds.length} sélectionné {selectedIds.length} sélectionné
{selectedIds.length > 1 ? "s" : ""} {selectedIds.length > 1 ? 's' : ''}
</span> </span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className="border-[#475569] text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="border-[#475569] text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
Bulk Actions Bulk Actions
<ChevronDown className="h-4 w-4 ml-2" /> <ChevronDown className="h-4 w-4 ml-2" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="bg-[#1E293B] border-[#475569]"> <DropdownMenuContent className="bg-[#1E293B] border-[#475569]">
<DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"> <DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]">
Exporter Exporter
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"> <DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]">
Supprimer Supprimer
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</> </>
)} )}
<Button className="bg-[#60A5FA] text-[#0F172A] hover:bg-[#3B82F6] rounded-[6px]"> <Button className="bg-[#60A5FA] text-[#0F172A] hover:bg-[#3B82F6] rounded-[6px]">
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Ajouter Ajouter
</Button> </Button>
</div> </div>
</div> </div>
{/* Table */} {/* Table */}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
<TableHeader className="sticky top-0 bg-[#1E293B] z-10"> <TableHeader className="sticky top-0 bg-[#1E293B] z-10">
<TableRow className="border-[#334155] hover:bg-[#1E293B]"> <TableRow className="border-[#334155] hover:bg-[#1E293B]">
<TableHead className="w-[40px] text-[#94A3B8]"> <TableHead className="w-[40px] text-[#94A3B8]">
<Checkbox <Checkbox
checked={ checked={
selectedIds.length === characters.length && selectedIds.length === characters.length &&
characters.length > 0 characters.length > 0
} }
onCheckedChange={toggleSelectAll} onCheckedChange={toggleSelectAll}
className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]"
/> />
</TableHead> </TableHead>
<TableHead className="text-[#94A3B8]"> <TableHead className="text-[#94A3B8]">
<button className="flex items-center gap-1 hover:text-[#F8FAFC] transition-colors"> <button className="flex items-center gap-1 hover:text-[#F8FAFC] transition-colors">
Nom Nom
<ArrowUpDown className="h-4 w-4" /> <ArrowUpDown className="h-4 w-4" />
</button> </button>
</TableHead> </TableHead>
<TableHead className="text-[#94A3B8]">Classe</TableHead> <TableHead className="text-[#94A3B8]">Classe</TableHead>
<TableHead className="text-[#94A3B8]"> <TableHead className="text-[#94A3B8]">
<button className="flex items-center gap-1 hover:text-[#F8FAFC] transition-colors"> <button className="flex items-center gap-1 hover:text-[#F8FAFC] transition-colors">
Niveau Niveau
<ArrowUpDown className="h-4 w-4" /> <ArrowUpDown className="h-4 w-4" />
</button> </button>
</TableHead> </TableHead>
<TableHead className="text-[#94A3B8]">Serveur</TableHead> <TableHead className="text-[#94A3B8]">Serveur</TableHead>
<TableHead className="text-[#94A3B8]">Compte</TableHead> <TableHead className="text-[#94A3B8]">Compte</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{characters.map((character) => { {characters.map((character) => {
const isSelected = selectedIds.includes(character.id); const isSelected = selectedIds.includes(character.id);
return ( return (
<TableRow <TableRow
key={character.id} key={character.id}
className={cn( className={cn(
"border-[#334155] h-12 transition-colors", 'border-[#334155] h-12 transition-colors',
isSelected isSelected
? "bg-[#1E293B] border-l-2 border-l-[#60A5FA]" ? 'bg-[#1E293B] border-l-2 border-l-[#60A5FA]'
: "hover:bg-[#1E293B]/50", : 'hover:bg-[#1E293B]/50',
)} )}
> >
<TableCell className="w-[40px]"> <TableCell className="w-[40px]">
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onCheckedChange={() => toggleSelect(character.id)} onCheckedChange={() => toggleSelect(character.id)}
className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]" className="border-[#475569] data-[state=checked]:bg-[#60A5FA] data-[state=checked]:border-[#60A5FA]"
/> />
</TableCell> </TableCell>
<TableCell className="text-[#F8FAFC] font-medium"> <TableCell className="text-[#F8FAFC] font-medium">
{character.nom} {character.nom}
</TableCell> </TableCell>
<TableCell className="text-[#F8FAFC]"> <TableCell className="text-[#F8FAFC]">
{character.classe} {character.classe}
</TableCell> </TableCell>
<TableCell className="text-[#F8FAFC]"> <TableCell className="text-[#F8FAFC]">
{character.niveau} {character.niveau}
</TableCell> </TableCell>
<TableCell className="text-[#F8FAFC]"> <TableCell className="text-[#F8FAFC]">
{character.serveur} {character.serveur}
</TableCell> </TableCell>
<TableCell className="text-[#F8FAFC]"> <TableCell className="text-[#F8FAFC]">
{character.compte} {character.compte}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
})} })}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
{/* Pagination Footer */} {/* Pagination Footer */}
<div className="flex items-center justify-between p-4 border-t border-[#334155] bg-[#0F172A]"> <div className="flex items-center justify-between p-4 border-t border-[#334155] bg-[#0F172A]">
<span className="text-sm text-[#94A3B8]">Showing 1-8 of 64</span> <span className="text-sm text-[#94A3B8]">Showing 1-8 of 64</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
{"<"} {'<'}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#60A5FA] bg-[#60A5FA]/10 text-[#60A5FA] hover:bg-[#60A5FA]/20 rounded-[6px]" className="border-[#60A5FA] bg-[#60A5FA]/10 text-[#60A5FA] hover:bg-[#60A5FA]/20 rounded-[6px]"
> >
1 1
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
2 2
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
3 3
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent" className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
> >
{">"} {'>'}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,54 +1,54 @@
import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from '@/components/ui/dialog';
import { Button } from "@/components/ui/button";
interface ConfirmationModalProps { interface ConfirmationModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
progressionName: string; progressionName: string;
incompleteCount: number; incompleteCount: number;
} }
export function ConfirmationModal({ export function ConfirmationModal({
open, open,
onOpenChange, onOpenChange,
progressionName, progressionName,
incompleteCount, incompleteCount,
}: ConfirmationModalProps) { }: ConfirmationModalProps) {
const handleConfirm = () => { const handleConfirm = () => {
// Do NOT implement actual update logic // Do NOT implement actual update logic
onOpenChange(false); onOpenChange(false);
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[400px] rounded-lg"> <DialogContent className="max-w-[400px] rounded-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Confirmer la mise à jour</DialogTitle> <DialogTitle>Confirmer la mise à jour</DialogTitle>
<DialogDescription> <DialogDescription>
Marquer {progressionName} comme fait pour {incompleteCount}{" "} Marquer {progressionName} comme fait pour {incompleteCount}{' '}
personnages ? personnages ?
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="gap-2 sm:gap-0"> <DialogFooter className="gap-2 sm:gap-0">
<Button <Button
variant="secondary" variant="secondary"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="rounded-md" className="rounded-md"
> >
Annuler Annuler
</Button> </Button>
<Button onClick={handleConfirm} className="rounded-md"> <Button onClick={handleConfirm} className="rounded-md">
Confirmer Confirmer
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }

View File

@@ -1,72 +1,72 @@
import { useLocation } from "@tanstack/react-router"; import { useLocation } from '@tanstack/react-router';
import { Button } from "@/components/ui/button"; import { ChevronRight, Moon, Sun } from 'lucide-react';
import { Moon, Sun, ChevronRight } from "lucide-react"; import { Button } from '@/components/ui/button';
const routeLabels: Record<string, string> = { const routeLabels: Record<string, string> = {
"/": "Dashboard", '/': 'Dashboard',
"/characters": "Personnages", '/characters': 'Personnages',
"/accounts": "Comptes", '/accounts': 'Comptes',
"/teams": "Teams", '/teams': 'Teams',
"/settings": "Paramètres", '/settings': 'Paramètres',
}; };
interface AppHeaderProps { interface AppHeaderProps {
theme: "dark" | "light"; theme: 'dark' | 'light';
onToggleTheme: () => void; onToggleTheme: () => void;
} }
export function AppHeader({ theme, onToggleTheme }: AppHeaderProps) { export function AppHeader({ theme, onToggleTheme }: AppHeaderProps) {
const { pathname } = useLocation(); const { pathname } = useLocation();
// Generate breadcrumb from pathname // Generate breadcrumb from pathname
const segments = pathname.split("/").filter(Boolean); const segments = pathname.split('/').filter(Boolean);
const breadcrumbs = const breadcrumbs =
segments.length === 0 segments.length === 0
? [{ label: "Dashboard", href: "/" }] ? [{ label: 'Dashboard', href: '/' }]
: segments.map((segment, index) => { : segments.map((segment, index) => {
const href = "/" + segments.slice(0, index + 1).join("/"); const href = '/' + segments.slice(0, index + 1).join('/');
const label = const label =
routeLabels[href] || routeLabels[href] ||
segment.charAt(0).toUpperCase() + segment.slice(1); segment.charAt(0).toUpperCase() + segment.slice(1);
return { label, href }; return { label, href };
}); });
return ( return (
<header className="flex h-16 items-center justify-between border-b border-border bg-card px-6"> <header className="flex h-16 items-center justify-between border-b border-border bg-card px-6">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-1 text-sm"> <nav className="flex items-center gap-1 text-sm">
{breadcrumbs.map((crumb, index) => ( {breadcrumbs.map((crumb, index) => (
<span key={crumb.href} className="flex items-center gap-1"> <span key={crumb.href} className="flex items-center gap-1">
{index > 0 && ( {index > 0 && (
<ChevronRight className="h-4 w-4 text-muted-foreground" /> <ChevronRight className="h-4 w-4 text-muted-foreground" />
)} )}
<span <span
className={ className={
index === breadcrumbs.length - 1 index === breadcrumbs.length - 1
? "font-medium text-foreground" ? 'font-medium text-foreground'
: "text-muted-foreground" : 'text-muted-foreground'
} }
> >
{crumb.label} {crumb.label}
</span> </span>
</span> </span>
))} ))}
</nav> </nav>
{/* Theme toggle */} {/* Theme toggle */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onToggleTheme} onClick={onToggleTheme}
className="h-9 w-9" className="h-9 w-9"
> >
{theme === "dark" ? ( {theme === 'dark' ? (
<Sun className="h-5 w-5" /> <Sun className="h-5 w-5" />
) : ( ) : (
<Moon className="h-5 w-5" /> <Moon className="h-5 w-5" />
)} )}
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</header> </header>
); );
} }

View File

@@ -1,53 +1,53 @@
import type React from "react"; import type React from 'react';
import { useState, useEffect } from "react"; import { useEffect, useState } from 'react';
import { cn } from "@/lib/utils"; import { AppHeader } from '@/components/layout/app-header';
import { AppSidebar } from "@/components/layout/app-sidebar"; import { AppSidebar } from '@/components/layout/app-sidebar';
import { AppHeader } from "@/components/layout/app-header"; import { cn } from '@/lib/utils';
interface AppShellProps { interface AppShellProps {
children: React.ReactNode; children: React.ReactNode;
} }
export function AppShell({ children }: AppShellProps) { export function AppShell({ children }: AppShellProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [theme, setTheme] = useState<"dark" | "light">("dark"); const [theme, setTheme] = useState<'dark' | 'light'>('dark');
// Apply theme class to html element // Apply theme class to html element
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
if (theme === "light") { if (theme === 'light') {
root.classList.add("light"); root.classList.add('light');
} else { } else {
root.classList.remove("light"); root.classList.remove('light');
} }
}, [theme]); }, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark")); setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
}; };
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<AppSidebar <AppSidebar
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)} onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
/> />
{/* Main content area */} {/* Main content area */}
<div <div
className={cn( className={cn(
"flex min-h-screen flex-col transition-all duration-200", 'flex min-h-screen flex-col transition-all duration-200',
sidebarCollapsed ? "pl-16" : "pl-60", sidebarCollapsed ? 'pl-16' : 'pl-60',
)} )}
> >
<AppHeader theme={theme} onToggleTheme={toggleTheme} /> <AppHeader theme={theme} onToggleTheme={toggleTheme} />
{/* Scrollable main content */} {/* Scrollable main content */}
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
<div className="mx-auto max-w-[1280px] p-6">{children}</div> <div className="mx-auto max-w-[1280px] p-6">{children}</div>
</main> </main>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,107 +1,107 @@
import { Link, useLocation } from "@tanstack/react-router"; import { Link, useLocation } from '@tanstack/react-router';
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { import {
Tooltip, ChevronLeft,
TooltipContent, ChevronRight,
TooltipProvider, Folder,
TooltipTrigger, Home,
} from "@/components/ui/tooltip"; Settings,
Swords,
Users,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { import {
Home, Tooltip,
Users, TooltipContent,
Folder, TooltipProvider,
Swords, TooltipTrigger,
Settings, } from '@/components/ui/tooltip';
ChevronLeft, import { cn } from '@/lib/utils';
ChevronRight,
} from "lucide-react";
const navItems = [ const navItems = [
{ icon: Home, label: "Dashboard", href: "/" }, { icon: Home, label: 'Dashboard', href: '/' },
{ icon: Users, label: "Personnages", href: "/characters" }, { icon: Users, label: 'Personnages', href: '/characters' },
{ icon: Folder, label: "Comptes", href: "/accounts" }, { icon: Folder, label: 'Comptes', href: '/accounts' },
{ icon: Swords, label: "Teams", href: "/teams" }, { icon: Swords, label: 'Teams', href: '/teams' },
{ icon: Settings, label: "Paramètres", href: "/settings" }, { icon: Settings, label: 'Paramètres', href: '/settings' },
]; ];
interface AppSidebarProps { interface AppSidebarProps {
collapsed: boolean; collapsed: boolean;
onToggle: () => void; onToggle: () => void;
} }
export function AppSidebar({ collapsed, onToggle }: AppSidebarProps) { export function AppSidebar({ collapsed, onToggle }: AppSidebarProps) {
const { pathname } = useLocation(); const { pathname } = useLocation();
return ( return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<aside <aside
className={cn( className={cn(
"fixed left-0 top-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200", 'fixed left-0 top-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200',
collapsed ? "w-16" : "w-60", collapsed ? 'w-16' : 'w-60',
)} )}
> >
{/* Header with logo and toggle */} {/* Header with logo and toggle */}
<div className="flex h-16 items-center justify-between border-b border-sidebar-border px-3"> <div className="flex h-16 items-center justify-between border-b border-sidebar-border px-3">
{!collapsed && ( {!collapsed && (
<span className="text-lg font-bold tracking-tight text-sidebar-foreground"> <span className="text-lg font-bold tracking-tight text-sidebar-foreground">
DOFUS MANAGER DOFUS MANAGER
</span> </span>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onToggle} onClick={onToggle}
className={cn( className={cn(
"h-8 w-8 text-sidebar-foreground hover:bg-sidebar-accent", 'h-8 w-8 text-sidebar-foreground hover:bg-sidebar-accent',
collapsed && "mx-auto", collapsed && 'mx-auto',
)} )}
> >
{collapsed ? ( {collapsed ? (
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
) : ( ) : (
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
)} )}
<span className="sr-only">Toggle sidebar</span> <span className="sr-only">Toggle sidebar</span>
</Button> </Button>
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 space-y-1 p-2"> <nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = pathname === item.href; const isActive = pathname === item.href;
const Icon = item.icon; const Icon = item.icon;
const linkContent = ( const linkContent = (
<Link <Link
to={item.href} to={item.href}
className={cn( className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive isActive
? "bg-sidebar-primary text-sidebar-primary-foreground" ? 'bg-sidebar-primary text-sidebar-primary-foreground'
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", : 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
)} )}
> >
<Icon className="h-5 w-5 shrink-0" /> <Icon className="h-5 w-5 shrink-0" />
{!collapsed && <span>{item.label}</span>} {!collapsed && <span>{item.label}</span>}
</Link> </Link>
); );
if (collapsed) { if (collapsed) {
return ( return (
<Tooltip key={item.href}> <Tooltip key={item.href}>
<TooltipTrigger asChild>{linkContent}</TooltipTrigger> <TooltipTrigger asChild>{linkContent}</TooltipTrigger>
<TooltipContent side="right" className="font-medium"> <TooltipContent side="right" className="font-medium">
{item.label} {item.label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
} }
return <div key={item.href}>{linkContent}</div>; return <div key={item.href}>{linkContent}</div>;
})} })}
</nav> </nav>
</aside> </aside>
</TooltipProvider> </TooltipProvider>
); );
} }

View File

@@ -1,273 +1,273 @@
import type React from "react";
import { import {
FolderOpen, ArrowRight,
Users, BarChart3,
Swords, ChevronDown,
Coins, Coins,
BarChart3, FolderOpen,
Plus, Plus,
ChevronDown, RefreshCw,
ArrowRight, Swords,
RefreshCw, Users,
} from "lucide-react"; } from 'lucide-react';
import type React from 'react';
import { Button } from '@/components/ui/button';
import { import {
Card, Card,
CardContent, CardContent,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from '@/components/ui/card';
import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from '@/components/ui/dropdown-menu';
// Custom progress bar with color support // Custom progress bar with color support
function ColoredProgress({ function ColoredProgress({
value, value,
color, color,
}: { }: {
value: number; value: number;
color: "success" | "warning" | "info"; color: 'success' | 'warning' | 'info';
}) { }) {
const colorClasses = { const colorClasses = {
success: "bg-[#4ADE80]", success: 'bg-[#4ADE80]',
warning: "bg-[#FBBF24]", warning: 'bg-[#FBBF24]',
info: "bg-[#60A5FA]", info: 'bg-[#60A5FA]',
}; };
return ( return (
<div className="relative h-2 w-full overflow-hidden rounded-full bg-[#334155]"> <div className="relative h-2 w-full overflow-hidden rounded-full bg-[#334155]">
<div <div
className={`h-full transition-all ${colorClasses[color]}`} className={`h-full transition-all ${colorClasses[color]}`}
style={{ width: `${value}%` }} style={{ width: `${value}%` }}
/> />
</div> </div>
); );
} }
// Stat card component with hover effect // Stat card component with hover effect
function StatCard({ function StatCard({
icon: Icon, icon: Icon,
title, title,
mainStat, mainStat,
secondary, secondary,
linkText, linkText,
children, children,
className = "", className = '',
}: { }: {
icon: React.ElementType; icon: React.ElementType;
title: string; title: string;
mainStat?: string; mainStat?: string;
secondary?: React.ReactNode; secondary?: React.ReactNode;
linkText: string; linkText: string;
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
}) { }) {
return ( return (
<Card <Card
className={`border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01] ${className}`} className={`border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01] ${className}`}
> >
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10"> <div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10">
<Icon className="size-5 text-[#60A5FA]" /> <Icon className="size-5 text-[#60A5FA]" />
</div> </div>
<CardTitle className="text-base font-semibold text-[#F8FAFC]"> <CardTitle className="text-base font-semibold text-[#F8FAFC]">
{title} {title}
</CardTitle> </CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-1"> <CardContent className="space-y-1">
{mainStat && ( {mainStat && (
<p className="text-2xl font-bold text-[#F8FAFC]">{mainStat}</p> <p className="text-2xl font-bold text-[#F8FAFC]">{mainStat}</p>
)} )}
{secondary && <div className="text-sm text-[#94A3B8]">{secondary}</div>} {secondary && <div className="text-sm text-[#94A3B8]">{secondary}</div>}
{children} {children}
</CardContent> </CardContent>
<CardFooter className="pt-2"> <CardFooter className="pt-2">
<button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline"> <button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline">
{linkText} {linkText}
<ArrowRight className="size-4" /> <ArrowRight className="size-4" />
</button> </button>
</CardFooter> </CardFooter>
</Card> </Card>
); );
} }
export function Dashboard() { export function Dashboard() {
// Mock data // Mock data
const currencies = [ const currencies = [
{ name: "Doplons", amount: "12,450" }, { name: 'Doplons', amount: '12,450' },
{ name: "Orichor", amount: "3,200" }, { name: 'Orichor', amount: '3,200' },
{ name: "Kamas glace", amount: "8,100" }, { name: 'Kamas glace', amount: '8,100' },
{ name: "Nuggets", amount: "2,340" }, { name: 'Nuggets', amount: '2,340' },
]; ];
const progressions = [ const progressions = [
{ label: "Dofus", value: 72, color: "success" as const }, { label: 'Dofus', value: 72, color: 'success' as const },
{ label: "Donjons", value: 45, color: "warning" as const }, { label: 'Donjons', value: 45, color: 'warning' as const },
{ label: "Recherchés", value: 61, color: "info" as const }, { label: 'Recherchés', value: 61, color: 'info' as const },
]; ];
return ( return (
<div className="min-h-screen bg-[#0F172A] p-6"> <div className="min-h-screen bg-[#0F172A] p-6">
{/* Header */} {/* Header */}
<header className="mb-8 flex items-center justify-between"> <header className="mb-8 flex items-center justify-between">
<h1 className="text-[32px] font-bold text-[#F8FAFC]">DASHBOARD</h1> <h1 className="text-[32px] font-bold text-[#F8FAFC]">DASHBOARD</h1>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90"> <Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90">
<Plus className="size-4" /> <Plus className="size-4" />
Nouveau Nouveau
<ChevronDown className="size-4" /> <ChevronDown className="size-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="border-[#334155] bg-[#1E293B]"> <DropdownMenuContent className="border-[#334155] bg-[#1E293B]">
<DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"> <DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]">
Personnage Personnage
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"> <DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]">
Compte Compte
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]"> <DropdownMenuItem className="text-[#F8FAFC] focus:bg-[#334155] focus:text-[#F8FAFC]">
Team Team
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</header> </header>
{/* Widget Grid */} {/* Widget Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
{/* Comptes Card */} {/* Comptes Card */}
<StatCard <StatCard
icon={FolderOpen} icon={FolderOpen}
title="Comptes" title="Comptes"
mainStat="12 comptes" mainStat="12 comptes"
secondary="45,230 ogrines" secondary="45,230 ogrines"
linkText="Voir tout" linkText="Voir tout"
/> />
{/* Personnages Card */} {/* Personnages Card */}
<StatCard <StatCard
icon={Users} icon={Users}
title="Personnages" title="Personnages"
mainStat="64 personnages" mainStat="64 personnages"
secondary="Niv. moy: 198" secondary="Niv. moy: 198"
linkText="Voir tout" linkText="Voir tout"
/> />
{/* Teams Card */} {/* Teams Card */}
<StatCard <StatCard
icon={Swords} icon={Swords}
title="Teams" title="Teams"
mainStat="3 actives" mainStat="3 actives"
secondary={ secondary={
<div className="space-y-1"> <div className="space-y-1">
<span>87% complete</span> <span>87% complete</span>
<ColoredProgress value={87} color="info" /> <ColoredProgress value={87} color="info" />
</div> </div>
} }
linkText="Voir tout" linkText="Voir tout"
/> />
{/* Monnaies Card - spans 2 columns on desktop */} {/* Monnaies Card - spans 2 columns on desktop */}
<Card className="border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01] xl:col-span-2"> <Card className="border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01] xl:col-span-2">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10"> <div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10">
<Coins className="size-5 text-[#60A5FA]" /> <Coins className="size-5 text-[#60A5FA]" />
</div> </div>
<CardTitle className="text-base font-semibold text-[#F8FAFC]"> <CardTitle className="text-base font-semibold text-[#F8FAFC]">
Monnaies Monnaies
</CardTitle> </CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4"> <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{currencies.map((currency) => ( {currencies.map((currency) => (
<div key={currency.name} className="space-y-1"> <div key={currency.name} className="space-y-1">
<p className="text-sm text-[#94A3B8]">{currency.name}</p> <p className="text-sm text-[#94A3B8]">{currency.name}</p>
<p className="text-xl font-semibold text-[#F8FAFC]"> <p className="text-xl font-semibold text-[#F8FAFC]">
{currency.amount} {currency.amount}
</p> </p>
</div> </div>
))} ))}
</div> </div>
</CardContent> </CardContent>
<CardFooter className="pt-2"> <CardFooter className="pt-2">
<button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline"> <button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline">
Détail par compte Détail par compte
<ArrowRight className="size-4" /> <ArrowRight className="size-4" />
</button> </button>
</CardFooter> </CardFooter>
</Card> </Card>
{/* Progressions Card */} {/* Progressions Card */}
<Card className="border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01]"> <Card className="border-[#334155] bg-[#1E293B] transition-transform duration-150 hover:scale-[1.01]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10"> <div className="flex size-10 items-center justify-center rounded-lg bg-[#60A5FA]/10">
<BarChart3 className="size-5 text-[#60A5FA]" /> <BarChart3 className="size-5 text-[#60A5FA]" />
</div> </div>
<CardTitle className="text-base font-semibold text-[#F8FAFC]"> <CardTitle className="text-base font-semibold text-[#F8FAFC]">
Progressions Progressions
</CardTitle> </CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{progressions.map((prog) => ( {progressions.map((prog) => (
<div key={prog.label} className="space-y-2"> <div key={prog.label} className="space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-[#F8FAFC]">{prog.label}</span> <span className="text-[#F8FAFC]">{prog.label}</span>
<span className="text-[#94A3B8]">{prog.value}%</span> <span className="text-[#94A3B8]">{prog.value}%</span>
</div> </div>
<ColoredProgress value={prog.value} color={prog.color} /> <ColoredProgress value={prog.value} color={prog.color} />
</div> </div>
))} ))}
</CardContent> </CardContent>
<CardFooter className="pt-2"> <CardFooter className="pt-2">
<button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline"> <button className="flex items-center gap-1 text-sm text-[#60A5FA] hover:underline">
Bulk Update Bulk Update
<ArrowRight className="size-4" /> <ArrowRight className="size-4" />
</button> </button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
{/* Quick Actions Section */} {/* Quick Actions Section */}
<section className="mt-8"> <section className="mt-8">
<h2 className="mb-4 text-lg font-semibold text-[#F8FAFC]"> <h2 className="mb-4 text-lg font-semibold text-[#F8FAFC]">
Actions Rapides Actions Rapides
</h2> </h2>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90"> <Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90">
<Plus className="size-4" /> <Plus className="size-4" />
Personnage Personnage
</Button> </Button>
<Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90"> <Button className="gap-2 rounded-[6px] bg-[#60A5FA] text-[#0F172A] hover:bg-[#60A5FA]/90">
<Plus className="size-4" /> <Plus className="size-4" />
Team Team
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"
className="gap-2 rounded-[6px] border-[#334155] bg-[#1E293B] text-[#F8FAFC] hover:bg-[#334155]" className="gap-2 rounded-[6px] border-[#334155] bg-[#1E293B] text-[#F8FAFC] hover:bg-[#334155]"
> >
<BarChart3 className="size-4" /> <BarChart3 className="size-4" />
Bulk Progressions Bulk Progressions
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"
className="gap-2 rounded-[6px] border-[#334155] bg-[#1E293B] text-[#F8FAFC] hover:bg-[#334155]" className="gap-2 rounded-[6px] border-[#334155] bg-[#1E293B] text-[#F8FAFC] hover:bg-[#334155]"
> >
<RefreshCw className="size-4" /> <RefreshCw className="size-4" />
Sync DofusDB Sync DofusDB
</Button> </Button>
</div> </div>
</section> </section>
</div> </div>
); );
} }

View File

@@ -1,106 +1,106 @@
import { useState } from "react"; import { Check, ChevronDown, ChevronRight } from 'lucide-react';
import { ChevronDown, ChevronRight, Check } from "lucide-react"; import { useState } from 'react';
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from '@/components/ui/checkbox';
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from '@/components/ui/collapsible';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
interface ProgressionItem { interface ProgressionItem {
id: string; id: string;
name: string; name: string;
completed: boolean; completed: boolean;
completedDate?: string; completedDate?: string;
} }
interface ProgressionSectionProps { interface ProgressionSectionProps {
title: string; title: string;
items: ProgressionItem[]; items: ProgressionItem[];
filter: "all" | "done" | "not-done"; filter: 'all' | 'done' | 'not-done';
} }
export function ProgressionSection({ export function ProgressionSection({
title, title,
items, items,
filter, filter,
}: ProgressionSectionProps) { }: ProgressionSectionProps) {
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
const [localItems, setLocalItems] = useState(items); const [localItems, setLocalItems] = useState(items);
const filteredItems = localItems.filter((item) => { const filteredItems = localItems.filter((item) => {
if (filter === "done") return item.completed; if (filter === 'done') return item.completed;
if (filter === "not-done") return !item.completed; if (filter === 'not-done') return !item.completed;
return true; return true;
}); });
const completedCount = localItems.filter((item) => item.completed).length; const completedCount = localItems.filter((item) => item.completed).length;
const totalCount = localItems.length; const totalCount = localItems.length;
const handleToggle = (id: string) => { const handleToggle = (id: string) => {
setLocalItems((prev) => setLocalItems((prev) =>
prev.map((item) => prev.map((item) =>
item.id === id item.id === id
? { ? {
...item, ...item,
completed: !item.completed, completed: !item.completed,
completedDate: !item.completed completedDate: !item.completed
? new Date().toISOString().split("T")[0] ? new Date().toISOString().split('T')[0]
: undefined, : undefined,
} }
: item, : item,
), ),
); );
}; };
if (filteredItems.length === 0) return null; if (filteredItems.length === 0) return null;
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-2"> <Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-2">
<CollapsibleTrigger className="flex w-full items-center gap-2 py-2 text-left hover:bg-secondary/50 rounded-md px-2 transition-colors"> <CollapsibleTrigger className="flex w-full items-center gap-2 py-2 text-left hover:bg-secondary/50 rounded-md px-2 transition-colors">
{isOpen ? ( {isOpen ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" /> <ChevronDown className="h-4 w-4 text-muted-foreground" />
) : ( ) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" /> <ChevronRight className="h-4 w-4 text-muted-foreground" />
)} )}
<span className="font-medium text-foreground">{title}</span> <span className="font-medium text-foreground">{title}</span>
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">
({completedCount}/{totalCount}) ({completedCount}/{totalCount})
</span> </span>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pl-6"> <CollapsibleContent className="space-y-1 pl-6">
{filteredItems.map((item) => ( {filteredItems.map((item) => (
<div <div
key={item.id} key={item.id}
className="flex items-center gap-3 py-2 px-2 rounded-md hover:bg-secondary/30 transition-colors" className="flex items-center gap-3 py-2 px-2 rounded-md hover:bg-secondary/30 transition-colors"
> >
<Checkbox <Checkbox
id={item.id} id={item.id}
checked={item.completed} checked={item.completed}
onCheckedChange={() => handleToggle(item.id)} onCheckedChange={() => handleToggle(item.id)}
className="border-muted-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary" className="border-muted-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/> />
<label <label
htmlFor={item.id} htmlFor={item.id}
className={cn( className={cn(
"flex-1 text-sm cursor-pointer", 'flex-1 text-sm cursor-pointer',
item.completed ? "text-foreground" : "text-muted-foreground", item.completed ? 'text-foreground' : 'text-muted-foreground',
)} )}
> >
{item.name} {item.name}
</label> </label>
{item.completed ? ( {item.completed ? (
<div className="flex items-center gap-1.5 text-sm"> <div className="flex items-center gap-1.5 text-sm">
<Check className="h-3.5 w-3.5" style={{ color: "#4ADE80" }} /> <Check className="h-3.5 w-3.5" style={{ color: '#4ADE80' }} />
<span style={{ color: "#4ADE80" }}>{item.completedDate}</span> <span style={{ color: '#4ADE80' }}>{item.completedDate}</span>
</div> </div>
) : ( ) : (
<span className="text-muted-foreground text-sm"></span> <span className="text-muted-foreground text-sm"></span>
)} )}
</div> </div>
))} ))}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );
} }

View File

@@ -1,225 +1,225 @@
import { useState } from "react";
import { import {
ChevronRight, CheckCircle,
Pencil, ChevronRight,
Trash2, Pencil,
CheckCircle, Trash2,
XCircle, XCircle,
} from "lucide-react"; } from 'lucide-react';
import { Card, CardContent } from "@/components/ui/card"; import { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ConfirmationModal } from '@/components/confirmation-modal';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from '@/components/ui/select';
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from '@/components/ui/table';
import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ConfirmationModal } from "@/components/confirmation-modal";
// Mock data // Mock data
const teamMembers = [ const teamMembers = [
{ id: 1, name: "Krosmaster", completed: true, date: "2026-01-10" }, { id: 1, name: 'Krosmaster', completed: true, date: '2026-01-10' },
{ id: 2, name: "TankMaster", completed: true, date: "2026-01-10" }, { id: 2, name: 'TankMaster', completed: true, date: '2026-01-10' },
{ id: 3, name: "HealBot", completed: false, date: null }, { id: 3, name: 'HealBot', completed: false, date: null },
{ id: 4, name: "SramKiller", completed: false, date: null }, { id: 4, name: 'SramKiller', completed: false, date: null },
{ id: 5, name: "Eniripsa", completed: true, date: "2026-01-09" }, { id: 5, name: 'Eniripsa', completed: true, date: '2026-01-09' },
{ id: 6, name: "Sacrieur", completed: true, date: "2026-01-08" }, { id: 6, name: 'Sacrieur', completed: true, date: '2026-01-08' },
{ id: 7, name: "Pandawa", completed: true, date: "2026-01-10" }, { id: 7, name: 'Pandawa', completed: true, date: '2026-01-10' },
{ id: 8, name: "Eliotrope", completed: true, date: "2026-01-07" }, { id: 8, name: 'Eliotrope', completed: true, date: '2026-01-07' },
]; ];
const progressions = [ const progressions = [
{ id: "dofus-turquoise", name: "Dofus Turquoise" }, { id: 'dofus-turquoise', name: 'Dofus Turquoise' },
{ id: "donjon-bethel", name: "Donjon Bethel" }, { id: 'donjon-bethel', name: 'Donjon Bethel' },
{ id: "quete-ebene", name: "Quête Ébène" }, { id: 'quete-ebene', name: 'Quête Ébène' },
{ id: "succes-dimension", name: "Succès Dimension" }, { id: 'succes-dimension', name: 'Succès Dimension' },
]; ];
export default function TeamDetailPage() { export default function TeamDetailPage() {
const [selectedProgression, setSelectedProgression] = const [selectedProgression, setSelectedProgression] =
useState("dofus-turquoise"); useState('dofus-turquoise');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const completedCount = teamMembers.filter((m) => m.completed).length; const completedCount = teamMembers.filter((m) => m.completed).length;
const totalCount = teamMembers.length; const totalCount = teamMembers.length;
const incompleteCount = totalCount - completedCount; const incompleteCount = totalCount - completedCount;
const progressPercentage = Math.round((completedCount / totalCount) * 100); const progressPercentage = Math.round((completedCount / totalCount) * 100);
const selectedProgressionName = const selectedProgressionName =
progressions.find((p) => p.id === selectedProgression)?.name || ""; progressions.find((p) => p.id === selectedProgression)?.name || '';
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background p-6">
<div className="mx-auto max-w-4xl space-y-6"> <div className="mx-auto max-w-4xl space-y-6">
{/* Header with Breadcrumb and Actions */} {/* Header with Breadcrumb and Actions */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<nav className="flex items-center gap-1 text-sm text-muted-foreground"> <nav className="flex items-center gap-1 text-sm text-muted-foreground">
<span className="hover:text-foreground cursor-pointer">Teams</span> <span className="hover:text-foreground cursor-pointer">Teams</span>
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
<span className="text-foreground">Main Team</span> <span className="text-foreground">Main Team</span>
</nav> </nav>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="ghost" size="icon" className="h-8 w-8"> <Button variant="ghost" size="icon" className="h-8 w-8">
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
<span className="sr-only">Edit team</span> <span className="sr-only">Edit team</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-destructive hover:text-destructive" className="h-8 w-8 text-destructive hover:text-destructive"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
<span className="sr-only">Delete team</span> <span className="sr-only">Delete team</span>
</Button> </Button>
</div> </div>
</div> </div>
{/* Team Info Card */} {/* Team Info Card */}
<Card className="rounded-lg"> <Card className="rounded-lg">
<CardContent className="p-6"> <CardContent className="p-6">
<h1 className="text-2xl font-bold tracking-tight">MAIN TEAM</h1> <h1 className="text-2xl font-bold tracking-tight">MAIN TEAM</h1>
<p className="mt-1 text-sm text-muted-foreground">Type: Main</p> <p className="mt-1 text-sm text-muted-foreground">Type: Main</p>
<div className="mt-4 flex items-center gap-3 text-sm"> <div className="mt-4 flex items-center gap-3 text-sm">
<span>{totalCount} membres</span> <span>{totalCount} membres</span>
<span className="text-muted-foreground"></span> <span className="text-muted-foreground"></span>
<Badge <Badge
variant="secondary" variant="secondary"
className="bg-[#4ADE80]/20 text-[#4ADE80] hover:bg-[#4ADE80]/30" className="bg-[#4ADE80]/20 text-[#4ADE80] hover:bg-[#4ADE80]/30"
> >
Active Active
</Badge> </Badge>
<span className="text-muted-foreground"></span> <span className="text-muted-foreground"></span>
<span>{totalCount} comptes différents</span> <span>{totalCount} comptes différents</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Tabs */} {/* Tabs */}
<Tabs defaultValue="statut-progressions" className="w-full"> <Tabs defaultValue="statut-progressions" className="w-full">
<TabsList className="w-full justify-start rounded-lg bg-card"> <TabsList className="w-full justify-start rounded-lg bg-card">
<TabsTrigger value="membres" className="rounded-md"> <TabsTrigger value="membres" className="rounded-md">
Membres Membres
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="statut-progressions" className="rounded-md"> <TabsTrigger value="statut-progressions" className="rounded-md">
Statut Progressions Statut Progressions
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
{/* Membres Tab - Placeholder */} {/* Membres Tab - Placeholder */}
<TabsContent value="membres" className="mt-4"> <TabsContent value="membres" className="mt-4">
<Card className="rounded-lg"> <Card className="rounded-lg">
<CardContent className="p-6"> <CardContent className="p-6">
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Liste des membres à venir... Liste des membres à venir...
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
{/* Statut Progressions Tab */} {/* Statut Progressions Tab */}
<TabsContent value="statut-progressions" className="mt-4 space-y-4"> <TabsContent value="statut-progressions" className="mt-4 space-y-4">
{/* Progression Selector */} {/* Progression Selector */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="text-sm font-medium">Progression:</label> <label className="text-sm font-medium">Progression:</label>
<Select <Select
value={selectedProgression} value={selectedProgression}
onValueChange={setSelectedProgression} onValueChange={setSelectedProgression}
> >
<SelectTrigger className="w-[220px] rounded-md"> <SelectTrigger className="w-[220px] rounded-md">
<SelectValue placeholder="Sélectionner une progression" /> <SelectValue placeholder="Sélectionner une progression" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{progressions.map((prog) => ( {progressions.map((prog) => (
<SelectItem key={prog.id} value={prog.id}> <SelectItem key={prog.id} value={prog.id}>
{prog.name} {prog.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Progress Summary */} {/* Progress Summary */}
<Card className="rounded-lg"> <Card className="rounded-lg">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="font-medium"> <span className="font-medium">
{progressPercentage}% ({completedCount}/{totalCount}) {progressPercentage}% ({completedCount}/{totalCount})
</span> </span>
</div> </div>
<Progress <Progress
value={progressPercentage} value={progressPercentage}
className="h-4 rounded-md" className="h-4 rounded-md"
/> />
</div> </div>
<Button <Button
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
disabled={incompleteCount === 0} disabled={incompleteCount === 0}
className="rounded-md" className="rounded-md"
> >
Marquer tous comme fait Marquer tous comme fait
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Status Table */} {/* Status Table */}
<Card className="rounded-lg"> <Card className="rounded-lg">
<CardContent className="p-0"> <CardContent className="p-0">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="hover:bg-transparent"> <TableRow className="hover:bg-transparent">
<TableHead className="h-10">Perso</TableHead> <TableHead className="h-10">Perso</TableHead>
<TableHead className="h-10">Statut</TableHead> <TableHead className="h-10">Statut</TableHead>
<TableHead className="h-10">Date</TableHead> <TableHead className="h-10">Date</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{teamMembers.map((member) => ( {teamMembers.map((member) => (
<TableRow key={member.id} className="h-10"> <TableRow key={member.id} className="h-10">
<TableCell className="py-2 font-medium"> <TableCell className="py-2 font-medium">
{member.name} {member.name}
</TableCell> </TableCell>
<TableCell className="py-2"> <TableCell className="py-2">
{member.completed ? ( {member.completed ? (
<CheckCircle className="h-5 w-5 text-[#4ADE80]" /> <CheckCircle className="h-5 w-5 text-[#4ADE80]" />
) : ( ) : (
<XCircle className="h-5 w-5 text-[#F87171]" /> <XCircle className="h-5 w-5 text-[#F87171]" />
)} )}
</TableCell> </TableCell>
<TableCell className="py-2 text-muted-foreground"> <TableCell className="py-2 text-muted-foreground">
{member.date || "—"} {member.date || '—'}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
{/* Confirmation Modal */} {/* Confirmation Modal */}
<ConfirmationModal <ConfirmationModal
open={isModalOpen} open={isModalOpen}
onOpenChange={setIsModalOpen} onOpenChange={setIsModalOpen}
progressionName={selectedProgressionName} progressionName={selectedProgressionName}
incompleteCount={incompleteCount} incompleteCount={incompleteCount}
/> />
</div> </div>
); );
} }

View File

@@ -1,62 +1,62 @@
import * as React from "react" import { Slot } from '@radix-ui/react-slot';
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from 'class-variance-authority';
import { cva, type VariantProps } from "class-variance-authority" import type * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: "size-9", icon: 'size-9',
"icon-sm": "size-8", 'icon-sm': 'size-8',
"icon-lg": "size-10", 'icon-lg': 'size-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} },
) );
function Button({ function Button({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button';
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,92 +1,92 @@
import * as React from "react" import type * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn('leading-none font-semibold', className)}
{...props} {...props}
/> />
) );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-description" data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-content" data-slot="card-content"
className={cn("px-6", className)} className={cn('px-6', className)}
{...props} {...props}
/> />
) );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props} {...props}
/> />
) );
} }
export { export {
Card, Card,
CardHeader, CardHeader,
CardFooter, CardFooter,
CardTitle, CardTitle,
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };

View File

@@ -1,21 +1,21 @@
import * as React from "react" import type * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };

View File

@@ -1,114 +1,114 @@
import * as React from "react" import type * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Table({ className, ...props }: React.ComponentProps<"table">) { function Table({ className, ...props }: React.ComponentProps<'table'>) {
return ( return (
<div <div
data-slot="table-container" data-slot="table-container"
className="relative w-full overflow-x-auto" className="relative w-full overflow-x-auto"
> >
<table <table
data-slot="table" data-slot="table"
className={cn("w-full caption-bottom text-sm", className)} className={cn('w-full caption-bottom text-sm', className)}
{...props} {...props}
/> />
</div> </div>
) );
} }
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn('[&_tr]:border-b', className)}
{...props} {...props}
/> />
) );
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return ( return (
<tbody <tbody
data-slot="table-body" data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)} className={cn('[&_tr:last-child]:border-0', className)}
{...props} {...props}
/> />
) );
} }
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return ( return (
<tfoot <tfoot
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return ( return (
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableHead({ className, ...props }: React.ComponentProps<"th">) { function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return ( return (
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableCell({ className, ...props }: React.ComponentProps<"td">) { function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return ( return (
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableCaption({ function TableCaption({
className, className,
...props ...props
}: React.ComponentProps<"caption">) { }: React.ComponentProps<'caption'>) {
return ( return (
<caption <caption
data-slot="table-caption" data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)} className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props} {...props}
/> />
) );
} }
export { export {
Table, Table,
TableHeader, TableHeader,
TableBody, TableBody,
TableFooter, TableFooter,
TableHead, TableHead,
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@@ -1,13 +1,13 @@
import { createServerFn } from '@tanstack/react-start' import { createServerFn } from '@tanstack/react-start';
export const getPunkSongs = createServerFn({ export const getPunkSongs = createServerFn({
method: 'GET', method: 'GET',
}).handler(async () => [ }).handler(async () => [
{ id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' }, { id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' },
{ id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' }, { id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' },
{ id: 3, name: 'The Middle', artist: 'Jimmy Eat World' }, { id: 3, name: 'The Middle', artist: 'Jimmy Eat World' },
{ id: 4, name: 'My Own Worst Enemy', artist: 'Lit' }, { id: 4, name: 'My Own Worst Enemy', artist: 'Lit' },
{ id: 5, name: 'Fat Lip', artist: 'Sum 41' }, { id: 5, name: 'Fat Lip', artist: 'Sum 41' },
{ id: 6, name: 'All the Small Things', artist: 'blink-182' }, { id: 6, name: 'All the Small Things', artist: 'blink-182' },
{ id: 7, name: 'Beverly Hills', artist: 'Weezer' }, { id: 7, name: 'Beverly Hills', artist: 'Weezer' },
]) ]);

View File

@@ -1,18 +1,18 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined; prisma: PrismaClient | undefined;
}; };
export const prisma = export const prisma =
globalForPrisma.prisma ?? globalForPrisma.prisma ??
new PrismaClient({ new PrismaClient({
log: log:
process.env.NODE_ENV === "development" process.env.NODE_ENV === 'development'
? ["query", "error", "warn"] ? ['query', 'error', 'warn']
: ["error"], : ['error'],
}); });
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma; globalForPrisma.prisma = prisma;
} }

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@@ -8,212 +8,213 @@
// You should NOT make any changes in this file as it will be overwritten. // You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root';
import { Route as IndexRouteImport } from './routes/index' import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names';
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs' import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request';
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request' import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs';
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names' import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only';
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index' import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr';
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode' import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index';
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr' import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode';
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only' import { Route as IndexRouteImport } from './routes/index';
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({ const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
id: '/demo/start/server-funcs', id: '/demo/start/server-funcs',
path: '/demo/start/server-funcs', path: '/demo/start/server-funcs',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({ const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
id: '/demo/start/api-request', id: '/demo/start/api-request',
path: '/demo/start/api-request', path: '/demo/start/api-request',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({ const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
id: '/demo/api/names', id: '/demo/api/names',
path: '/demo/api/names', path: '/demo/api/names',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({ const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
id: '/demo/start/ssr/', id: '/demo/start/ssr/',
path: '/demo/start/ssr/', path: '/demo/start/ssr/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({ const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
id: '/demo/start/ssr/spa-mode', id: '/demo/start/ssr/spa-mode',
path: '/demo/start/ssr/spa-mode', path: '/demo/start/ssr/spa-mode',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({ const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
id: '/demo/start/ssr/full-ssr', id: '/demo/start/ssr/full-ssr',
path: '/demo/start/ssr/full-ssr', path: '/demo/start/ssr/full-ssr',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({ const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
id: '/demo/start/ssr/data-only', id: '/demo/start/ssr/data-only',
path: '/demo/start/ssr/data-only', path: '/demo/start/ssr/data-only',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute;
'/demo/api/names': typeof DemoApiNamesRoute '/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute '/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute '/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute;
'/demo/api/names': typeof DemoApiNamesRoute '/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute '/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr': typeof DemoStartSsrIndexRoute '/demo/start/ssr': typeof DemoStartSsrIndexRoute;
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport;
'/': typeof IndexRoute '/': typeof IndexRoute;
'/demo/api/names': typeof DemoApiNamesRoute '/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute '/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute '/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: fullPaths:
| '/' | '/'
| '/demo/api/names' | '/demo/api/names'
| '/demo/start/api-request' | '/demo/start/api-request'
| '/demo/start/server-funcs' | '/demo/start/server-funcs'
| '/demo/start/ssr/data-only' | '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr' | '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode' | '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/' | '/demo/start/ssr/';
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo;
to: to:
| '/' | '/'
| '/demo/api/names' | '/demo/api/names'
| '/demo/start/api-request' | '/demo/start/api-request'
| '/demo/start/server-funcs' | '/demo/start/server-funcs'
| '/demo/start/ssr/data-only' | '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr' | '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode' | '/demo/start/ssr/spa-mode'
| '/demo/start/ssr' | '/demo/start/ssr';
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/demo/api/names' | '/demo/api/names'
| '/demo/start/api-request' | '/demo/start/api-request'
| '/demo/start/server-funcs' | '/demo/start/server-funcs'
| '/demo/start/ssr/data-only' | '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr' | '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode' | '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/' | '/demo/start/ssr/';
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById;
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute;
DemoApiNamesRoute: typeof DemoApiNamesRoute DemoApiNamesRoute: typeof DemoApiNamesRoute;
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute;
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute;
DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute;
DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute;
DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute;
DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute;
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/': { '/': {
id: '/' id: '/';
path: '/' path: '/';
fullPath: '/' fullPath: '/';
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/server-funcs': { '/demo/start/server-funcs': {
id: '/demo/start/server-funcs' id: '/demo/start/server-funcs';
path: '/demo/start/server-funcs' path: '/demo/start/server-funcs';
fullPath: '/demo/start/server-funcs' fullPath: '/demo/start/server-funcs';
preLoaderRoute: typeof DemoStartServerFuncsRouteImport preLoaderRoute: typeof DemoStartServerFuncsRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/api-request': { '/demo/start/api-request': {
id: '/demo/start/api-request' id: '/demo/start/api-request';
path: '/demo/start/api-request' path: '/demo/start/api-request';
fullPath: '/demo/start/api-request' fullPath: '/demo/start/api-request';
preLoaderRoute: typeof DemoStartApiRequestRouteImport preLoaderRoute: typeof DemoStartApiRequestRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/api/names': { '/demo/api/names': {
id: '/demo/api/names' id: '/demo/api/names';
path: '/demo/api/names' path: '/demo/api/names';
fullPath: '/demo/api/names' fullPath: '/demo/api/names';
preLoaderRoute: typeof DemoApiNamesRouteImport preLoaderRoute: typeof DemoApiNamesRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/ssr/': { '/demo/start/ssr/': {
id: '/demo/start/ssr/' id: '/demo/start/ssr/';
path: '/demo/start/ssr' path: '/demo/start/ssr';
fullPath: '/demo/start/ssr/' fullPath: '/demo/start/ssr/';
preLoaderRoute: typeof DemoStartSsrIndexRouteImport preLoaderRoute: typeof DemoStartSsrIndexRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/ssr/spa-mode': { '/demo/start/ssr/spa-mode': {
id: '/demo/start/ssr/spa-mode' id: '/demo/start/ssr/spa-mode';
path: '/demo/start/ssr/spa-mode' path: '/demo/start/ssr/spa-mode';
fullPath: '/demo/start/ssr/spa-mode' fullPath: '/demo/start/ssr/spa-mode';
preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/ssr/full-ssr': { '/demo/start/ssr/full-ssr': {
id: '/demo/start/ssr/full-ssr' id: '/demo/start/ssr/full-ssr';
path: '/demo/start/ssr/full-ssr' path: '/demo/start/ssr/full-ssr';
fullPath: '/demo/start/ssr/full-ssr' fullPath: '/demo/start/ssr/full-ssr';
preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
'/demo/start/ssr/data-only': { '/demo/start/ssr/data-only': {
id: '/demo/start/ssr/data-only' id: '/demo/start/ssr/data-only';
path: '/demo/start/ssr/data-only' path: '/demo/start/ssr/data-only';
fullPath: '/demo/start/ssr/data-only' fullPath: '/demo/start/ssr/data-only';
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
} }
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
DemoApiNamesRoute: DemoApiNamesRoute, DemoApiNamesRoute: DemoApiNamesRoute,
DemoStartApiRequestRoute: DemoStartApiRequestRoute, DemoStartApiRequestRoute: DemoStartApiRequestRoute,
DemoStartServerFuncsRoute: DemoStartServerFuncsRoute, DemoStartServerFuncsRoute: DemoStartServerFuncsRoute,
DemoStartSsrDataOnlyRoute: DemoStartSsrDataOnlyRoute, DemoStartSsrDataOnlyRoute: DemoStartSsrDataOnlyRoute,
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute, DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute, DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute, DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
} };
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>();
import type { createStart } from '@tanstack/react-start';
import type { getRouter } from './router.tsx';
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' { declare module '@tanstack/react-start' {
interface Register { interface Register {
ssr: true ssr: true;
router: Awaited<ReturnType<typeof getRouter>> router: Awaited<ReturnType<typeof getRouter>>;
} }
} }

View File

@@ -1,17 +1,17 @@
import { createRouter } from '@tanstack/react-router' import { createRouter } from '@tanstack/react-router';
// Import the generated route tree // Import the generated route tree
import { routeTree } from './routeTree.gen' import { routeTree } from './routeTree.gen';
// Create a new router instance // Create a new router instance
export const getRouter = () => { export const getRouter = () => {
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: {}, context: {},
scrollRestoration: true, scrollRestoration: true,
defaultPreloadStaleTime: 0, defaultPreloadStaleTime: 0,
}) });
return router return router;
} };

View File

@@ -1,58 +1,58 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' import { TanStackDevtools } from '@tanstack/react-devtools';
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
import { TanStackDevtools } from '@tanstack/react-devtools' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
import Header from '../components/Header' import Header from '../components/Header';
import appCss from '../styles.css?url' import appCss from '../styles.css?url';
export const Route = createRootRoute({ export const Route = createRootRoute({
head: () => ({ head: () => ({
meta: [ meta: [
{ {
charSet: 'utf-8', charSet: 'utf-8',
}, },
{ {
name: 'viewport', name: 'viewport',
content: 'width=device-width, initial-scale=1', content: 'width=device-width, initial-scale=1',
}, },
{ {
title: 'TanStack Start Starter', title: 'TanStack Start Starter',
}, },
], ],
links: [ links: [
{ {
rel: 'stylesheet', rel: 'stylesheet',
href: appCss, href: appCss,
}, },
], ],
}), }),
shellComponent: RootDocument, shellComponent: RootDocument,
}) });
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<Header /> <Header />
{children} {children}
<TanStackDevtools <TanStackDevtools
config={{ config={{
position: 'bottom-right', position: 'bottom-right',
}} }}
plugins={[ plugins={[
{ {
name: 'Tanstack Router', name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />, render: <TanStackRouterDevtoolsPanel />,
}, },
]} ]}
/> />
<Scripts /> <Scripts />
</body> </body>
</html> </html>
) );
} }

View File

@@ -1,20 +1,20 @@
import { createAPIFileRoute } from "@tanstack/start/api"; import { createAPIFileRoute } from '@tanstack/start/api';
import { prisma } from "@/lib/server/db"; import { prisma } from '@/lib/server/db';
export const Route = createAPIFileRoute("/api/health")({ export const Route = createAPIFileRoute('/api/health')({
GET: async () => { GET: async () => {
let dbStatus = "disconnected"; let dbStatus = 'disconnected';
try { try {
await prisma.$queryRaw`SELECT 1`; await prisma.$queryRaw`SELECT 1`;
dbStatus = "connected"; dbStatus = 'connected';
} catch { } catch {
dbStatus = "error"; dbStatus = 'error';
} }
return Response.json({ return Response.json({
status: "ok", status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
database: dbStatus, database: dbStatus,
}); });
}, },
}); });

View File

@@ -1,10 +1,10 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router';
import { json } from '@tanstack/react-start' import { json } from '@tanstack/react-start';
export const Route = createFileRoute('/demo/api/names')({ export const Route = createFileRoute('/demo/api/names')({
server: { server: {
handlers: { handlers: {
GET: () => json(['Alice', 'Bob', 'Charlie']), GET: () => json(['Alice', 'Bob', 'Charlie']),
}, },
}, },
}) });

View File

@@ -1,33 +1,34 @@
import { useEffect, useState } from 'react' import { createFileRoute } from '@tanstack/react-router';
import { useEffect, useState } from 'react';
import { createFileRoute } from '@tanstack/react-router' import './start.css';
import './start.css'
function getNames() { function getNames() {
return fetch('/demo/api/names').then((res) => res.json() as Promise<string[]>) return fetch('/demo/api/names').then(
(res) => res.json() as Promise<string[]>,
);
} }
export const Route = createFileRoute('/demo/start/api-request')({ export const Route = createFileRoute('/demo/start/api-request')({
component: Home, component: Home,
}) });
function Home() { function Home() {
const [names, setNames] = useState<Array<string>>([]) const [names, setNames] = useState<Array<string>>([]);
useEffect(() => { useEffect(() => {
getNames().then(setNames) getNames().then(setNames);
}, []) }, []);
return ( return (
<div className="api-page"> <div className="api-page">
<div className="content"> <div className="content">
<h1>Start API Request Demo - Names List</h1> <h1>Start API Request Demo - Names List</h1>
<ul> <ul>
{names.map((name) => ( {names.map((name) => (
<li key={name}>{name}</li> <li key={name}>{name}</li>
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,8 +1,8 @@
import fs from 'node:fs' import fs from 'node:fs';
import { useCallback, useState } from 'react' import { createFileRoute, useRouter } from '@tanstack/react-router';
import { createFileRoute, useRouter } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start';
import { createServerFn } from '@tanstack/react-start' import { useCallback, useState } from 'react';
import './start.css' import './start.css';
/* /*
const loggingMiddleware = createMiddleware().server( const loggingMiddleware = createMiddleware().server(
@@ -16,77 +16,77 @@ const loggedServerFunction = createServerFn({ method: "GET" }).middleware([
]); ]);
*/ */
const TODOS_FILE = 'todos.json' const TODOS_FILE = 'todos.json';
async function readTodos() { async function readTodos() {
return JSON.parse( return JSON.parse(
await fs.promises.readFile(TODOS_FILE, 'utf-8').catch(() => await fs.promises.readFile(TODOS_FILE, 'utf-8').catch(() =>
JSON.stringify( JSON.stringify(
[ [
{ id: 1, name: 'Get groceries' }, { id: 1, name: 'Get groceries' },
{ id: 2, name: 'Buy a new phone' }, { id: 2, name: 'Buy a new phone' },
], ],
null, null,
2, 2,
), ),
), ),
) );
} }
const getTodos = createServerFn({ const getTodos = createServerFn({
method: 'GET', method: 'GET',
}).handler(async () => await readTodos()) }).handler(async () => await readTodos());
const addTodo = createServerFn({ method: 'POST' }) const addTodo = createServerFn({ method: 'POST' })
.inputValidator((d: string) => d) .inputValidator((d: string) => d)
.handler(async ({ data }) => { .handler(async ({ data }) => {
const todos = await readTodos() const todos = await readTodos();
todos.push({ id: todos.length + 1, name: data }) todos.push({ id: todos.length + 1, name: data });
await fs.promises.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2)) await fs.promises.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2));
return todos return todos;
}) });
export const Route = createFileRoute('/demo/start/server-funcs')({ export const Route = createFileRoute('/demo/start/server-funcs')({
component: Home, component: Home,
loader: async () => await getTodos(), loader: async () => await getTodos(),
}) });
function Home() { function Home() {
const router = useRouter() const router = useRouter();
let todos = Route.useLoaderData() let todos = Route.useLoaderData();
const [todo, setTodo] = useState('') const [todo, setTodo] = useState('');
const submitTodo = useCallback(async () => { const submitTodo = useCallback(async () => {
todos = await addTodo({ data: todo }) todos = await addTodo({ data: todo });
setTodo('') setTodo('');
router.invalidate() router.invalidate();
}, [addTodo, todo]) }, [addTodo, todo]);
return ( return (
<div> <div>
<h1>Start Server Functions - Todo Example</h1> <h1>Start Server Functions - Todo Example</h1>
<ul> <ul>
{todos?.map((t) => ( {todos?.map((t) => (
<li key={t.id}>{t.name}</li> <li key={t.id}>{t.name}</li>
))} ))}
</ul> </ul>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<input <input
type="text" type="text"
value={todo} value={todo}
onChange={(e) => setTodo(e.target.value)} onChange={(e) => setTodo(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
submitTodo() submitTodo();
} }
}} }}
placeholder="Enter a new todo..." placeholder="Enter a new todo..."
/> />
<button disabled={todo.trim().length === 0} onClick={submitTodo}> <button disabled={todo.trim().length === 0} onClick={submitTodo}>
Add todo Add todo
</button> </button>
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,25 +1,25 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router';
import { getPunkSongs } from '@/data/demo.punk-songs' import { getPunkSongs } from '@/data/demo.punk-songs';
export const Route = createFileRoute('/demo/start/ssr/data-only')({ export const Route = createFileRoute('/demo/start/ssr/data-only')({
ssr: 'data-only', ssr: 'data-only',
component: RouteComponent, component: RouteComponent,
loader: async () => await getPunkSongs(), loader: async () => await getPunkSongs(),
}) });
function RouteComponent() { function RouteComponent() {
const punkSongs = Route.useLoaderData() const punkSongs = Route.useLoaderData();
return ( return (
<div> <div>
<h1>Data Only SSR - Punk Songs</h1> <h1>Data Only SSR - Punk Songs</h1>
<ul> <ul>
{punkSongs.map((song) => ( {punkSongs.map((song) => (
<li key={song.id}> <li key={song.id}>
{song.name} - {song.artist} {song.name} - {song.artist}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
) );
} }

View File

@@ -1,24 +1,24 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router';
import { getPunkSongs } from '@/data/demo.punk-songs' import { getPunkSongs } from '@/data/demo.punk-songs';
export const Route = createFileRoute('/demo/start/ssr/full-ssr')({ export const Route = createFileRoute('/demo/start/ssr/full-ssr')({
component: RouteComponent, component: RouteComponent,
loader: async () => await getPunkSongs(), loader: async () => await getPunkSongs(),
}) });
function RouteComponent() { function RouteComponent() {
const punkSongs = Route.useLoaderData() const punkSongs = Route.useLoaderData();
return ( return (
<div> <div>
<h1>Full SSR - Punk Songs</h1> <h1>Full SSR - Punk Songs</h1>
<ul> <ul>
{punkSongs.map((song) => ( {punkSongs.map((song) => (
<li key={song.id}> <li key={song.id}>
{song.name} - {song.artist} {song.name} - {song.artist}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
) );
} }

View File

@@ -1,24 +1,24 @@
import { createFileRoute, Link } from '@tanstack/react-router' import { createFileRoute, Link } from '@tanstack/react-router';
export const Route = createFileRoute('/demo/start/ssr/')({ export const Route = createFileRoute('/demo/start/ssr/')({
component: RouteComponent, component: RouteComponent,
}) });
function RouteComponent() { function RouteComponent() {
return ( return (
<div> <div>
<h1>SSR Demos</h1> <h1>SSR Demos</h1>
<ul> <ul>
<li> <li>
<Link to="/demo/start/ssr/spa-mode">SPA Mode</Link> <Link to="/demo/start/ssr/spa-mode">SPA Mode</Link>
</li> </li>
<li> <li>
<Link to="/demo/start/ssr/full-ssr">Full SSR</Link> <Link to="/demo/start/ssr/full-ssr">Full SSR</Link>
</li> </li>
<li> <li>
<Link to="/demo/start/ssr/data-only">Data Only</Link> <Link to="/demo/start/ssr/data-only">Data Only</Link>
</li> </li>
</ul> </ul>
</div> </div>
) );
} }

View File

@@ -1,31 +1,31 @@
import { useEffect, useState } from 'react' import { createFileRoute } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router' import { useEffect, useState } from 'react';
import { getPunkSongs } from '@/data/demo.punk-songs' import { getPunkSongs } from '@/data/demo.punk-songs';
export const Route = createFileRoute('/demo/start/ssr/spa-mode')({ export const Route = createFileRoute('/demo/start/ssr/spa-mode')({
ssr: false, ssr: false,
component: RouteComponent, component: RouteComponent,
}) });
function RouteComponent() { function RouteComponent() {
const [punkSongs, setPunkSongs] = useState< const [punkSongs, setPunkSongs] = useState<
Awaited<ReturnType<typeof getPunkSongs>> Awaited<ReturnType<typeof getPunkSongs>>
>([]) >([]);
useEffect(() => { useEffect(() => {
getPunkSongs().then(setPunkSongs) getPunkSongs().then(setPunkSongs);
}, []) }, []);
return ( return (
<div> <div>
<h1>SPA Mode - Punk Songs</h1> <h1>SPA Mode - Punk Songs</h1>
<ul> <ul>
{punkSongs.map((song) => ( {punkSongs.map((song) => (
<li key={song.id}> <li key={song.id}>
{song.name} - {song.artist} {song.name} - {song.artist}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
) );
} }

View File

@@ -1,37 +1,37 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router';
import '../App.css' import '../App.css';
export const Route = createFileRoute('/')({ component: App }) export const Route = createFileRoute('/')({ component: App });
function App() { function App() {
return ( return (
<div className="App"> <div className="App">
<header className="App-header"> <header className="App-header">
<img <img
src="/tanstack-circle-logo.png" src="/tanstack-circle-logo.png"
className="App-logo" className="App-logo"
alt="TanStack Logo" alt="TanStack Logo"
/> />
<p> <p>
Edit <code>src/routes/index.tsx</code> and save to reload. Edit <code>src/routes/index.tsx</code> and save to reload.
</p> </p>
<a <a
className="App-link" className="App-link"
href="https://reactjs.org" href="https://reactjs.org"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Learn React Learn React
</a> </a>
<a <a
className="App-link" className="App-link"
href="https://tanstack.com" href="https://tanstack.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Learn TanStack Learn TanStack
</a> </a>
</header> </header>
</div> </div>
) );
} }