From 079d2010de74800116a0a8e4dc49e123efa36b1a Mon Sep 17 00:00:00 2001 From: globuzma Date: Thu, 21 Nov 2024 17:15:45 +0100 Subject: [PATCH] Feat: Add Bset structure and Bset creation page --- app/app/account/profile/page.tsx | 10 + app/app/admin/bsets/page.tsx | 192 ++++++++++ app/app/api/admin/bsets/create/route.ts | 69 ++++ app/app/api/admin/bsets/delete/route.ts | 57 +++ app/app/api/admin/bsets/route.ts | 46 +++ app/app/api/admin/bsets/update/route.ts | 45 +++ app/app/api/admin/sets/route.ts | 48 +++ app/components/ui/badge.tsx | 36 ++ app/components/ui/checkbox.tsx | 29 ++ app/components/ui/command.tsx | 152 ++++++++ app/components/ui/dialog.tsx | 121 ++++++ app/components/ui/table.tsx | 120 ++++++ app/package-lock.json | 465 ++++++++++++++++++++++++ app/package.json | 3 + app/prisma/schema.prisma | 61 ++-- app/tools/updateDatabase.mjs | 34 +- 16 files changed, 1459 insertions(+), 29 deletions(-) create mode 100644 app/app/admin/bsets/page.tsx create mode 100644 app/app/api/admin/bsets/create/route.ts create mode 100644 app/app/api/admin/bsets/delete/route.ts create mode 100644 app/app/api/admin/bsets/route.ts create mode 100644 app/app/api/admin/bsets/update/route.ts create mode 100644 app/app/api/admin/sets/route.ts create mode 100644 app/components/ui/badge.tsx create mode 100644 app/components/ui/checkbox.tsx create mode 100644 app/components/ui/command.tsx create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/table.tsx diff --git a/app/app/account/profile/page.tsx b/app/app/account/profile/page.tsx index 313e5f5..b0a78ca 100644 --- a/app/app/account/profile/page.tsx +++ b/app/app/account/profile/page.tsx @@ -33,6 +33,16 @@ export default function Signin() { <>

Mon compte

+ + + Administration + + + + + + + Profile diff --git a/app/app/admin/bsets/page.tsx b/app/app/admin/bsets/page.tsx new file mode 100644 index 0000000..963f87d --- /dev/null +++ b/app/app/admin/bsets/page.tsx @@ -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([]) + const [bsetList, setBsetList] = useState([]) + const [selectedSets, setSelectedSets] = useState([]) + const BSetNameInput = useRef(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 ( +
+
+ + + + + Créer un nouveau BSet + +
+ +
+ {selectedSets.map((set) => ( + {removeSetToBSet(set.name_en)}} className="flex flex-row gap-2 cursor-pointer" key={set.id}>{set.name_en}x + ))} +
+ + + + Pas d’extension trouvée... + + {setList.map((set) => ( + + + {set.name_en} + + ))} + + + +
+ + + + + + + + +
+
+ + + + Name + Extensions + Actions + + + + { bsetList.map((bset) => ( + + {bset.name} + + { bset.sets.map((set) => ( + {set.name_en} + ))} + + + + ))} + +
+
+
+ ); +} diff --git a/app/app/api/admin/bsets/create/route.ts b/app/app/api/admin/bsets/create/route.ts new file mode 100644 index 0000000..8e17153 --- /dev/null +++ b/app/app/api/admin/bsets/create/route.ts @@ -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, + } + ); + } +} diff --git a/app/app/api/admin/bsets/delete/route.ts b/app/app/api/admin/bsets/delete/route.ts new file mode 100644 index 0000000..76fef16 --- /dev/null +++ b/app/app/api/admin/bsets/delete/route.ts @@ -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, + } + ); + } +} diff --git a/app/app/api/admin/bsets/route.ts b/app/app/api/admin/bsets/route.ts new file mode 100644 index 0000000..bde7727 --- /dev/null +++ b/app/app/api/admin/bsets/route.ts @@ -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, + } + ); + } +} diff --git a/app/app/api/admin/bsets/update/route.ts b/app/app/api/admin/bsets/update/route.ts new file mode 100644 index 0000000..df3c3e0 --- /dev/null +++ b/app/app/api/admin/bsets/update/route.ts @@ -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, + } + ); + } +} diff --git a/app/app/api/admin/sets/route.ts b/app/app/api/admin/sets/route.ts new file mode 100644 index 0000000..bf69f0c --- /dev/null +++ b/app/app/api/admin/sets/route.ts @@ -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, + } + ); + } +} diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/app/components/ui/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/app/components/ui/checkbox.tsx b/app/components/ui/checkbox.tsx new file mode 100644 index 0000000..83692e2 --- /dev/null +++ b/app/components/ui/checkbox.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/app/components/ui/command.tsx b/app/components/ui/command.tsx new file mode 100644 index 0000000..ade5441 --- /dev/null +++ b/app/components/ui/command.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..b5aaef7 --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/app/components/ui/table.tsx b/app/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/app/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/app/package-lock.json b/app/package-lock.json index 4c5fbc5..3f86135 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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", diff --git a/app/package.json b/app/package.json index 012f1a7..9a776a4 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 536117f..b805b66 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + previewFeatures = ["relationJoins"] } datasource db { @@ -16,26 +17,42 @@ model utilisateurice { } model carte { - id String @id @default(uuid()) @db.Uuid - scryfall_id String - name_en String - name_fr String - released_at String - small_image String - normal_image String - mana_cost String - cmc Int - type_line_en String? - type_line_fr String? - oracle_text_en String? - oracle_text_fr String? - power String? - toughness String? - colors String[] - keywords String[] - set String - set_name_en String - set_type String - rarity String - cardmarket_uri String? + id String @id @default(uuid()) @db.Uuid + name_en String + name_fr String + released_at String + small_image String + normal_image String + mana_cost String + cmc Int + type_line_en String? + type_line_fr String? + oracle_text_en String? + oracle_text_fr String? + power String? + toughness String? + colors String[] + keywords 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[] } diff --git a/app/tools/updateDatabase.mjs b/app/tools/updateDatabase.mjs index cfcc557..a933860 100644 --- a/app/tools/updateDatabase.mjs +++ b/app/tools/updateDatabase.mjs @@ -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