biome lint applied
This commit is contained in:
70
biome.json
70
biome.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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' },
|
||||||
])
|
]);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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']),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user