Compare commits

...

2 commits

Author SHA1 Message Date
globuzma
cbaf27321c Fix: Add typo and icons in Navigation Bar 2024-11-21 17:16:21 +01:00
globuzma
079d2010de Feat: Add Bset structure and Bset creation page 2024-11-21 17:15:45 +01:00
21 changed files with 1683 additions and 56 deletions

View file

@ -33,6 +33,16 @@ export default function Signin() {
<>
<div className="flex flex-col items-center mt-24" >
<h1 className="text-3xl mb-4">Mon compte</h1>
<Card className="w-full max-w-xs">
<CardHeader>
<CardTitle>Administration</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<a href="/admin/bsets" className="w-full"><Button className="w-auto">Éditer les BSets</Button></a>
<Button className="w-auto" disabled={true}>Éditer les cartes</Button>
<Button className="w-auto" disabled={true}>Éditer les joueur·euses</Button>
</CardContent>
</Card>
<Card className="w-full max-w-xs">
<CardHeader>
<CardTitle>Profile</CardTitle>

View file

@ -0,0 +1,192 @@
'use client'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogClose,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useState, useEffect, useRef } from "react"
import { getCookie } from "@/lib/utils"
import type { set, bset } from "@prisma/client";
interface bsetExtended extends bset {
sets: set[]
}
export default function Home() {
const [setList, setSetList] = useState<set[]>([])
const [bsetList, setBsetList] = useState<bsetExtended[]>([])
const [selectedSets, setSelectedSets] = useState<set[]>([])
const BSetNameInput = useRef<HTMLInputElement>(null)
const token = getCookie('JWT')
useEffect(() => {
fetch('http://localhost:3000/api/admin/sets', {
method: "GET",
headers: {Authorization: 'Bearer ' + token}
}).then((res) => {
console.log(res.status)
if(res.status == 200) {
res.json().then((setsData) => {
setSetList(setsData.data)
})
}
})
fetch('http://localhost:3000/api/admin/bsets', {
method: "GET",
headers: {Authorization: 'Bearer ' + token}
}).then((res) => {
console.log(res.status)
if(res.status == 200) {
res.json().then((bsetsData) => {
setBsetList(bsetsData.data)
})
}
})
}, [])
function addSetToBSet(currentValue: string) {
const selected = setList.filter((set: set) => set.name_en == currentValue)[0]
const isAlreadyIn = selectedSets.filter((set: set) => set.name_en == currentValue).length != 0
if(!isAlreadyIn){
setSelectedSets(old => [...old, selected])
}
}
function removeSetToBSet(currentValue: string) {
setSelectedSets(old => old.filter((set: set) => set.name_en != currentValue))
}
function createBSet(){
const bset_name = BSetNameInput.current!.value
const data : { bset_name: string, selected_sets: string[] } = { bset_name , selected_sets: [] }
selectedSets.forEach((set: set) => data.selected_sets.push(set.id))
BSetNameInput.current!.value = ""
fetch('http://localhost:3000/api/admin/bsets/create', {
method: "POST",
headers: {Authorization: 'Bearer ' + token},
body: JSON.stringify(data)
}).then((res) => {
if(res.status == 200) {
res.json().then((apiData) => {
setBsetList(old => [...old, { id: apiData.data, name: bset_name, sets: selectedSets }])
setSelectedSets([])
})
}
})
}
function deleteBSet(id: string){
fetch('http://localhost:3000/api/admin/bsets/delete', {
method: "DELETE",
headers: {Authorization: 'Bearer ' + token},
body: JSON.stringify({ id })
}).then((res) => {
if(res.status == 200) {
setBsetList(old => old.filter((bset) => bset.id != id))
}
})
}
return (
<div className="flex flex-col w-full items-center mt-32">
<div className="max-w-3xl w-full flex flex-col items-center gap-8">
<Dialog>
<DialogTrigger asChild><Button>Créer un nouveau BSet</Button></DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Créer un nouveau BSet</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<Input ref={BSetNameInput} placeholder="Nom du BSet" />
<div className="flex flex-row flex-wrap gap-4">
{selectedSets.map((set) => (
<Badge onClick={() => {removeSetToBSet(set.name_en)}} className="flex flex-row gap-2 cursor-pointer" key={set.id}><span>{set.name_en}</span><span>x</span></Badge>
))}
</div>
<Command>
<CommandInput placeholder="Rechercher une extension..." />
<CommandList>
<CommandEmpty>Pas dextension trouvée...</CommandEmpty>
<CommandGroup>
{setList.map((set) => (
<CommandItem
key={set.id}
value={set.name_en}
onSelect={addSetToBSet}
>
<img src={set.icon_svg_uri} loading="lazy" className="w-8 h-8" />
{set.name_en}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</div>
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={createBSet} type="button" variant="secondary">
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<Table className="max-w-3xl">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Extensions</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ bsetList.map((bset) => (
<TableRow key={bset.id}>
<TableCell>{bset.name}</TableCell>
<TableCell className="flex flex-row gap-4 items-center mt-2 flex-wrap">
{ bset.sets.map((set) => (
<Badge key={set.id}>{set.name_en}</Badge>
))}
</TableCell>
<TableCell><Button onClick={() => {deleteBSet(bset.id)}} variant="destructive">Supprimer</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}

View file

@ -0,0 +1,69 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken, decryptToken } from '@/lib/jwt'
import { db } from "@/lib/db"
interface orFilterProps {
id: string
}
export async function POST(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
const { bset_name, selected_sets } = await req.json()
if(token !== undefined) {
if(validateToken(token)) {
const tokenData = decryptToken(token)
if(tokenData.admin == true){
if(bset_name != undefined && selected_sets != undefined) {
const bset = await db.bset.create({
data: {
name: bset_name
}
})
const or_filter: orFilterProps[] = []
selected_sets.forEach((set_id: string) => {
or_filter.push({ id: set_id })
})
await db.set.updateMany({
where: {
OR: or_filter
},
data: {
bset_id: bset.id
}
})
return NextResponse.json({"data": bset.id, "message": "BSet created !"},{
status: 200,
});
}
} else {
return NextResponse.json({"message": "You're not admin."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

View file

@ -0,0 +1,57 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken, decryptToken } from '@/lib/jwt'
import { db } from "@/lib/db"
export async function DELETE(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
const { id } = await req.json()
if(token !== undefined) {
if(validateToken(token)) {
const tokenData = decryptToken(token)
if(tokenData.admin == true){
if(id != undefined) {
await db.set.updateMany({
where: {
bset_id: id
},
data: {
bset_id: null
}
})
await db.bset.delete({
where: { id }
})
return NextResponse.json({"message": "BSet deleted"},{
status: 200,
});
}
} else {
return NextResponse.json({"message": "You're not admin."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

View file

@ -0,0 +1,46 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken, decryptToken } from '@/lib/jwt'
import { db } from '@/lib/db'
export async function GET(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
if(token !== undefined) {
if(validateToken(token)) {
const tokenData = decryptToken(token)
if(tokenData.admin == true){
const bsets = await db.bset.findMany({
relationLoadStrategy: "join",
include: {
sets: true
}
})
return NextResponse.json({"data": bsets},{
status: 200,
});
} else {
return NextResponse.json({"message": "You're not admin."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

View file

@ -0,0 +1,45 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken, decryptToken } from '@/lib/jwt'
import { db } from "@/lib/db"
export async function GET(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
if(token !== undefined) {
if(validateToken(token)) {
const tokenData = decryptToken(token)
if(tokenData.admin == true){
const sets = await db.set.findMany({
where: {
set_type: "expansion"
}
})
return NextResponse.json({"data": sets,"message": "You're admin ! Congrats !"},{
status: 200,
});
} else {
return NextResponse.json({"message": "You're not admin."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

View file

@ -0,0 +1,48 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken, decryptToken } from '@/lib/jwt'
import { db } from "@/lib/db"
export async function GET(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
if(token !== undefined) {
if(validateToken(token)) {
const tokenData = decryptToken(token)
if(tokenData.admin == true){
const sets = await db.set.findMany({
where: {
AND: [
{ set_type: "expansion"},
{ bset_id: null}
]
}
})
return NextResponse.json({"data": sets,"message": "You're admin ! Congrats !"},{
status: 200,
});
} else {
return NextResponse.json({"message": "You're not admin."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

Binary file not shown.

View file

@ -6,6 +6,15 @@ body {
font-family: Arial, Helvetica, sans-serif;
}
@font-face {
font-family: "Beleren";
src: url("./fonts/Beleren2016-Bold.woff");
}
.font-beleren {
font-family: 'Beleren';
}
@layer utilities {
.text-balance {
text-wrap: balance;

View file

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "@radix-ui/react-icons"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,152 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View file

@ -0,0 +1,121 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,97 @@
import { cn } from "@/lib/utils"
interface manaIcon {
className: string
}
const Black = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/B.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
Black.displayName = "Black"
const Blue = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/U.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
Blue.displayName = "Blue"
const Green = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/G.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
Green.displayName = "Green"
const White = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/W.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
White.displayName = "White"
const Red = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/R.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
Red.displayName = "Red"
const Colorless = ({ className, ...props }: manaIcon) => {
return (
<img
src="https://svgs.scryfall.io/card-symbols/C.svg"
loading="lazy"
className={cn(
"",
className
)}
{...props}
/>
)
}
Colorless.displayName = "Colorless"
export { Black, Blue, Green, White, Red, Colorless }

View file

@ -17,6 +17,7 @@ import {
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { IconUserFilled } from "@tabler/icons-react"
import { Black, Blue, Green, White, Red, Colorless } from "@/components/ui/mana-icons"
interface NavigationProps {
isLoggedIn: boolean,
@ -27,7 +28,10 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
return (
<div className="flex flex-row p-4 gap-4 w-full fixed top-0 left-0 bg-slate-700 items-center justify-between">
<div className="flex flex-row gap-4 items-center">
<a href="/">Brawl Set</a>
<a className="flex flex-row gap-2 items-center" href="/">
<img src="/assets/logo.png" className="h-8" />
<span className="font-beleren text-3xl mt-2">BRAWL SET</span>
</a>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Cartes</Button>
@ -50,21 +54,27 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<White className="h-4 w-4"/>
<span>Blanc</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Blue className="h-4 w-4"/>
<span>Bleu</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Black className="h-4 w-4"/>
<span>Noir</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Red className="h-4 w-4"/>
<span>Rouge</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Green className="h-4 w-4"/>
<span>Vert</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Colorless className="h-4 w-4"/>
<span>Incolor</span>
</DropdownMenuItem>
<DropdownMenuItem>
@ -149,21 +159,27 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<White className="h-4 w-4"/>
<span>Blanc</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Blue className="h-4 w-4"/>
<span>Bleu</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Black className="h-4 w-4"/>
<span>Noir</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Red className="h-4 w-4"/>
<span>Rouge</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Green className="h-4 w-4"/>
<span>Vert</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Colorless className="h-4 w-4"/>
<span>Incolor</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
@ -176,34 +192,54 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<span>WU Azorius</span>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<span>Azorius</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>UB Dimir</span>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<span>Dimir</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>BR Rakdos</span>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<span>Rakdos</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>RG Gruul</span>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>Gruul</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>GW Selesnya</span>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<span>Selesnya</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>WB Orzhov</span>
<White className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<span>Orzhov</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>UR Izzet</span>
<Blue className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<span>Izzet</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>BG Golgari</span>
<Black className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>Golgari</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>RW Boros</span>
<Red className="h-4 w-4"/>
<White className="h-4 w-4"/>
<span>Boros</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>GU Simic</span>
<Green className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<span>Simic</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
@ -215,34 +251,64 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<span>WUB Esper</span>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<span>Esper</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>UBR Grixis</span>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<span>Grixis</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>BRG Jund</span>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>Jund</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>RGW Naya</span>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<span>Naya</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>GWU Bant</span>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<span>Bant</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>WBG Abzan</span>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>Abzan</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>URW Jeskai</span>
<Blue className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<White className="h-4 w-4"/>
<span>Jeskai</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>BGU Sultai</span>
<Black className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<span>Sultai</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>RWB Mardu</span>
<Red className="h-4 w-4"/>
<White className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<span>Mardu</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>GUR Temur</span>
<Green className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<span>Temur</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
@ -254,22 +320,47 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<span>WUBR Yore-Tiller</span>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<span>Yore-Tiller</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>UBRG Glint-Eye</span>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>Glint-Eye</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>BRGW Dune-Brood</span>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<span>Dune-Brood</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>RGWU Ink-Treader</span>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<span>Ink-Treader</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>GWUB Witch-Maw</span>
<Green className="h-4 w-4"/>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<span>Witch-Maw</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>WBBRG 5 couleurs</span>
<White className="h-4 w-4"/>
<Blue className="h-4 w-4"/>
<Black className="h-4 w-4"/>
<Red className="h-4 w-4"/>
<Green className="h-4 w-4"/>
<span>5 couleurs</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>

120
app/components/ui/table.tsx Normal file
View file

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

465
app/package-lock.json generated
View file

@ -10,6 +10,8 @@
"dependencies": {
"@prisma/client": "^5.22.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
@ -18,6 +20,7 @@
"@tabler/icons-react": "^3.22.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dotenv": "^16.4.5",
"lucide-react": "^0.453.0",
"next": "14.2.15",
@ -52,6 +55,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
@ -1026,6 +1041,36 @@
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz",
"integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
@ -1097,6 +1142,42 @@
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
"integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@ -2482,6 +2563,384 @@
"node": ">=6"
}
},
"node_modules/cmdk": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.3",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5666,6 +6125,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",

View file

@ -11,6 +11,8 @@
"dependencies": {
"@prisma/client": "^5.22.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
@ -19,6 +21,7 @@
"@tabler/icons-react": "^3.22.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dotenv": "^16.4.5",
"lucide-react": "^0.453.0",
"next": "14.2.15",

View file

@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}
datasource db {
@ -17,7 +18,6 @@ model utilisateurice {
model carte {
id String @id @default(uuid()) @db.Uuid
scryfall_id String
name_en String
name_fr String
released_at String
@ -33,9 +33,26 @@ model carte {
toughness String?
colors String[]
keywords String[]
set String
set_name_en String
set_type String
set set @relation(fields: [set_id], references: [id])
set_id String @db.Uuid
rarity String
cardmarket_uri String?
}
model set {
id String @id @default(uuid()) @db.Uuid
name_en String
code String
set_type String
released_at String?
icon_svg_uri String
cards carte[]
bset bset? @relation(fields: [bset_id], references: [id])
bset_id String? @db.Uuid
}
model bset {
id String @id @default(uuid()) @db.Uuid
name String
sets set[]
}

BIN
app/public/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,8 +1,14 @@
import 'dotenv/config'
import 'https'
import fs from 'fs'
import pg from 'pg'
const { Client } = pg
const scryfallSets = await fetch('https://api.scryfall.com/sets');
console.log('Status Code:', scryfallSets.status);
const sets = await scryfallSets.json();
// Read the data from the exported fr_cards.json extracted from Scryfall Bulk Data
const fileBytes = fs.readFileSync(import.meta.dirname + '/data/fr_cards.json')
let scryfallData = JSON.parse(fileBytes)
@ -18,12 +24,26 @@ const client = new Client({
await client.connect()
try {
const setRes = await client.query('SELECT id FROM set')
const preUpdateSetRows = setRes.rows
let preUpdateSetIds = []
preUpdateSetRows.forEach(element => {
preUpdateSetIds.push(element.id)
});
for (const set of sets.data) {
if(!preUpdateSetIds.includes(set.id)){
const addingSetQuery = await client.query('INSERT INTO set(id, name_en, code, set_type, released_at, icon_svg_uri) VALUES($1, $2, $3, $4, $5, $6)', [set.id, set.name, set.code, set.set_type, set.released_at, set.icon_svg_uri])
}
}
// Select already imported cards in database
const res = await client.query('SELECT scryfall_id FROM carte')
const preUpdateRows = res.rows
let preUpdateIds = []
preUpdateRows.forEach(element => {
preUpdateIds.push(element.scryfall_id)
const cardsRes = await client.query('SELECT id FROM carte')
const preUpdateCardsRows = cardsRes.rows
let preUpdateCardsIds = []
preUpdateCardsRows.forEach(element => {
preUpdateCardsIds.push(element.id)
});
// Define counter for logging
@ -33,7 +53,7 @@ try {
// For each card check if we need to upload it to the database
for (const carte of scryfallData) {
if(!preUpdateIds.includes(carte.id)){
if(!preUpdateCardsIds.includes(carte.id)){
if(carte.printed_name == undefined) {
// If the card doesn't have a french name, print it to the console and skip
@ -45,7 +65,7 @@ try {
}
// Add the card to the database
const addingQuery = await client.query('INSERT INTO carte(id, scryfall_id, name_en, name_fr, released_at, small_image, normal_image, mana_cost, cmc, type_line_en, type_line_fr, oracle_text_en, oracle_text_fr, power, toughness, colors, keywords, set, set_name_en, set_type, rarity, cardmarket_uri) VALUES(gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)', [carte.id, carte.name, carte.printed_name, carte.released_at, carte.image_uris.small, carte.image_uris.normal, carte.mana_cost, carte.cmc, carte.type_line, carte.printed_type_line, carte.oracle_text, carte.printed_text, carte.power, carte.toughness, carte.colors, carte.keywords, carte.set, carte.set_name, carte.set_type, carte.rarity, carte.purchase_uris?.cardmarket])
const addingCardsQuery = await client.query('INSERT INTO carte(id, name_en, name_fr, released_at, small_image, normal_image, mana_cost, cmc, type_line_en, type_line_fr, oracle_text_en, oracle_text_fr, power, toughness, colors, keywords, set_id, rarity, cardmarket_uri) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)', [carte.id, carte.name, carte.printed_name, carte.released_at, carte.image_uris.small, carte.image_uris.normal, carte.mana_cost, carte.cmc, carte.type_line, carte.printed_type_line, carte.oracle_text, carte.printed_text, carte.power, carte.toughness, carte.colors, carte.keywords, carte.set_id, carte.rarity, carte.purchase_uris?.cardmarket])
total_inserted = total_inserted + 1
} else {
total_skipped = total_skipped + 1