440 lines
16 KiB
TypeScript
440 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogClose,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
|
|
import { useEffect, useState, useRef } from "react"
|
|
import { useRouter } from 'next/navigation'
|
|
import { getCookie } from "@/lib/utils"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { MTGCardTextHover } from "@/components/ui/mtg-card-text-hover"
|
|
import { Toaster } from "@/components/ui/toaster"
|
|
import { useToast } from "@/hooks/use-toast"
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/deck-accordion"
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command"
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover"
|
|
import { Spinner } from "@/components/ui/spinner"
|
|
|
|
import type { deck, bset, carte, cartes_dans_deck } from "@prisma/client";
|
|
|
|
interface bsetJson extends bset {
|
|
set_codes: string[],
|
|
icons: string[]
|
|
}
|
|
|
|
interface deckExtended extends deck {
|
|
commander: carte
|
|
cartes: cartes_dans_deckExtended[]
|
|
}
|
|
|
|
interface cartes_dans_deckExtended extends cartes_dans_deck {
|
|
carte: carte
|
|
}
|
|
|
|
interface cardEntryAPIProps {
|
|
amount: number,
|
|
sanitized_name: string,
|
|
}
|
|
|
|
interface deckAPIProps {
|
|
id?: string, // For Update API
|
|
name: string,
|
|
selected_bset?: string, // For Create API
|
|
url?: string,
|
|
commander_name?: string,
|
|
cards: cardEntryAPIProps[]
|
|
}
|
|
|
|
export default function Signin() {
|
|
const [deckName, setDeckName] = useState("")
|
|
const [deckUrl, setDeckUrl] = useState("")
|
|
const [deckCommanderName, setDeckCommanderName] = useState("")
|
|
const [deckImporter, setDeckImporter] = useState("")
|
|
const [selectedBset, setSelectedBset] = useState("")
|
|
const [decks, setDecks] = useState<deckExtended[]>([])
|
|
const [displayedDecks, setDisplayedDecks] = useState<deckExtended[]>([])
|
|
|
|
const [loading, setLoading] = useState<boolean>(true)
|
|
|
|
const [bsets, setBsets] = useState<bsetJson[]>([])
|
|
|
|
const [openSelectBset, setOpenSelectBset] = useState(false)
|
|
const router = useRouter()
|
|
const token = getCookie('JWT')
|
|
|
|
const [deckIdToDelete, setDeckIdToDelete] = useState("")
|
|
const deleteDialogRef = useRef<HTMLButtonElement>(null)
|
|
|
|
const [editDeckUrl, setEditDeckUrl] = useState("")
|
|
const [editDeckId, setEditDeckId] = useState("")
|
|
const [editDeckName, setEditDeckName] = useState("")
|
|
const [editDeckImporter, setEditDeckImporter] = useState("")
|
|
const [editDeckCommanderName, setEditDeckCommanderName] = useState("")
|
|
const editDialogRef = useRef<HTMLButtonElement>(null)
|
|
|
|
const { toast } = useToast()
|
|
|
|
useEffect(() => {
|
|
if(getCookie('JWT') == "") {
|
|
router.refresh()
|
|
router.push('/')
|
|
}
|
|
|
|
fetch('/api/account/decks/', {
|
|
method: "GET",
|
|
headers: {Authorization: 'Bearer ' + token},
|
|
}).then((res) => {
|
|
if(res.status == 200) {
|
|
res.json().then((apiData) => {
|
|
console.log(apiData.data)
|
|
setDecks(apiData.data)
|
|
setDisplayedDecks(apiData.data)
|
|
setLoading(false)
|
|
})
|
|
}
|
|
})
|
|
|
|
fetch('/api/json/misc/bsets.json').then((res) => {
|
|
if(res.status == 200) {
|
|
res.json().then((data) => {
|
|
setBsets(data)
|
|
})
|
|
}
|
|
})
|
|
|
|
},[])
|
|
|
|
function getDataFromLine(line: string){
|
|
if(line != "") {
|
|
const data = line.split(" ")
|
|
const amount = parseInt(data[0])
|
|
const name = data.slice(1).join(" ").split("/")[0].replace(/[^a-zA-Z0-9]/gim,"-").toLowerCase()
|
|
return {"sanitized_name":name, "amount":amount}
|
|
} else {
|
|
return null
|
|
}
|
|
|
|
}
|
|
|
|
function deleteDeck(id:string){
|
|
fetch('/api/account/decks/delete', {
|
|
method: "DELETE",
|
|
headers: {Authorization: 'Bearer ' + token},
|
|
body: JSON.stringify({ id })
|
|
}).then((res) => {
|
|
if(res.status == 200) {
|
|
setDecks(old => old.filter((deck: deck) => deck.id != id))
|
|
setDisplayedDecks(old => old.filter((deck: deck) => deck.id != id))
|
|
toast({
|
|
title: "Deck supprimé",
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function updateDeckInput(txt:string){
|
|
setDeckImporter(txt)
|
|
const lines = txt.split("\n")
|
|
setDeckCommanderName(lines[lines.length - 1].slice(1))
|
|
}
|
|
|
|
function updateEditDeckInput(txt:string){
|
|
setEditDeckImporter(txt)
|
|
const lines = txt.split("\n")
|
|
setEditDeckCommanderName(lines[lines.length - 1].slice(1))
|
|
}
|
|
|
|
function editDialogHandler(id:string){
|
|
const deck_to_edit = decks.find((deck: deck) => deck.id === id)
|
|
setEditDeckId(id)
|
|
setEditDeckName(deck_to_edit ? deck_to_edit.name : "")
|
|
setEditDeckUrl(deck_to_edit && deck_to_edit.url != null ? deck_to_edit.url : "")
|
|
editDialogRef.current?.click()
|
|
}
|
|
|
|
function importDeck(){
|
|
const deckText = deckImporter
|
|
let lines = deckText.split("\n")
|
|
lines = lines.filter((line) => line.match(/[0-9]+\s[\w]+/) != undefined)
|
|
const dataToSend : deckAPIProps = { name: deckName, url: deckUrl, selected_bset: selectedBset.replace(/[^a-zA-Z0-9]/gim,"-").toLowerCase() ,commander_name: getDataFromLine(deckCommanderName)!.sanitized_name, cards: [] }
|
|
lines.slice(0, lines.length - 1).forEach((line: string) => {
|
|
const data = getDataFromLine(line)
|
|
if(data != null) {
|
|
dataToSend.cards.push(data)
|
|
}
|
|
});
|
|
|
|
fetch('/api/account/decks/create', {
|
|
method: "POST",
|
|
headers: {Authorization: 'Bearer ' + token},
|
|
body: JSON.stringify(dataToSend)
|
|
}).then((res) => {
|
|
if(res.status == 200) {
|
|
res.json().then((apiData) => {
|
|
const new_deck: deckExtended = apiData.data
|
|
setDisplayedDecks([...decks, new_deck])
|
|
setDecks(oldDecks => [...oldDecks, new_deck])
|
|
setDeckName("")
|
|
setDeckImporter("")
|
|
setSelectedBset("")
|
|
setDeckCommanderName("")
|
|
setDeckUrl("")
|
|
toast({
|
|
title: "Deck " + new_deck.name + " créé.",
|
|
})
|
|
})
|
|
} else if (res.status == 401) {
|
|
res.json().then((apiData) => {
|
|
toast({
|
|
title: "ERROR : " + apiData.message,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function updateDeck(){
|
|
const deckText = editDeckImporter
|
|
let lines = deckText.split("\n")
|
|
lines = lines.filter((line) => line.match(/[0-9]+\s[\w]+/) != undefined)
|
|
const dataToSend : deckAPIProps = { id: editDeckId, name: editDeckName, url: editDeckUrl, commander_name: editDeckCommanderName != "" ? getDataFromLine(editDeckCommanderName)!.sanitized_name : undefined, cards: [] }
|
|
if (editDeckImporter != "") {
|
|
lines.slice(0, lines.length - 1).forEach((line: string) => {
|
|
const data = getDataFromLine(line)
|
|
if(data != null) {
|
|
dataToSend.cards.push(data)
|
|
}
|
|
})
|
|
}
|
|
console.log(dataToSend)
|
|
|
|
fetch('/api/account/decks/update', {
|
|
method: "PUT",
|
|
headers: {Authorization: 'Bearer ' + token},
|
|
body: JSON.stringify(dataToSend)
|
|
}).then((res) => {
|
|
if(res.status == 200) {
|
|
res.json().then((apiData) => {
|
|
const new_deck = apiData.data
|
|
|
|
const displayedDecksIndex = displayedDecks.findIndex((deck) => deck.id == new_deck.id)
|
|
if (displayedDecksIndex != -1) { setDisplayedDecks(oldDecks => [...oldDecks.slice(0, displayedDecksIndex),new_deck,...oldDecks.slice(displayedDecksIndex + 1)]) }
|
|
|
|
const decksIndex = decks.findIndex((deck) => deck.id == new_deck.id)
|
|
if (decksIndex != -1) { setDecks(oldDecks => [...oldDecks.slice(0, decksIndex),new_deck,...oldDecks.slice(decksIndex + 1)]) }
|
|
|
|
console.log(apiData)
|
|
toast({
|
|
title: "Deck " + new_deck.name + " mis à jour.",
|
|
})
|
|
})
|
|
} else if (res.status == 401) {
|
|
res.json().then((apiData) => {
|
|
toast({
|
|
title: "ERROR : " + apiData.message,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function deleteAlertHandler(id:string, mode:string) {
|
|
if (mode == "set") {
|
|
deleteDialogRef.current?.click()
|
|
} else if (mode == "delete") {
|
|
deleteDeck(deckIdToDelete)
|
|
}
|
|
setDeckIdToDelete(id)
|
|
}
|
|
|
|
function filterDecks(searchString:string) {
|
|
if(searchString != "") {
|
|
setDisplayedDecks(decks.filter((deck) => deck.name.toLowerCase().includes(searchString.toLowerCase())))
|
|
} else {
|
|
setDisplayedDecks(decks)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col items-center mt-24" >
|
|
<div className="flex flex-col max-w-5xl w-full gap-12 items-center">
|
|
<div className="flex flex-row gap-12 w-full">
|
|
<div className="flex flex-col gap-4 grow-4 w-full">
|
|
<Textarea value={deckImporter} onChange={(e) => updateDeckInput(e.target.value)} placeholder="Deck List dans le format MTGO. Exemple : 1 Agate-Blade Assassin 1 Agate-Blade Assassin 1 Agate-Blade Assassin 1 Agate-Blade Assassin" className="h-60" />
|
|
<div><span className="font-bold">Commandant :</span> <span className="italic">{deckCommanderName}</span></div>
|
|
</div>
|
|
<div className="flex flex-col gap-4 w-full">
|
|
<Input className="text-3xl" value={deckName} onChange={(e) => setDeckName(e.target.value)} placeholder="Nom du deck" />
|
|
<Input placeholder="URL du Deck (Facultatif)" />
|
|
<Popover open={openSelectBset} onOpenChange={setOpenSelectBset}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openSelectBset}
|
|
className="w-full justify-between"
|
|
>
|
|
{ selectedBset && (
|
|
<div className="flex flex-row gap-2 items-center">
|
|
<div className="flex flex-row gap-1">
|
|
{ bsets.find((bset: bset) => bset.sanitized_name === selectedBset)?.icons.map((icon) => (
|
|
<img key={icon} src={icon} className="h-3" loading="lazy" />
|
|
))}
|
|
</div>
|
|
<span>{ bsets.find((bset: bset) => bset.sanitized_name === selectedBset)?.name }</span>
|
|
</div>
|
|
)}
|
|
{ !selectedBset && (
|
|
<span>Selectionnez un Bset...</span>
|
|
)}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent>
|
|
<Command>
|
|
<CommandInput placeholder="Search framework..." />
|
|
<CommandList>
|
|
<CommandEmpty>Pas de BSet trouvé.</CommandEmpty>
|
|
<CommandGroup>
|
|
{bsets.map((bset) => (
|
|
<CommandItem
|
|
key={bset.sanitized_name}
|
|
value={bset.sanitized_name}
|
|
onSelect={(currentValue) => {
|
|
setSelectedBset(currentValue === selectedBset ? "" : currentValue)
|
|
setOpenSelectBset(false)
|
|
}}
|
|
>
|
|
<div className="flex flex-row gap-2">
|
|
{bset.icons.map((icon) => (
|
|
<img key={icon} src={icon} className="h-3" loading="lazy" />
|
|
))}
|
|
</div>
|
|
{bset.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<Button onClick={importDeck}>Importer</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit Dialog */}
|
|
<Dialog>
|
|
<DialogTrigger className="hidden" asChild>
|
|
<Button ref={editDialogRef} variant="outline">Edit Profile</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Modifier le deck</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-4">
|
|
<Input className="text-3xl" value={editDeckName} onChange={(e) => setEditDeckName(e.target.value)} placeholder="Nom du deck" />
|
|
<Input value={editDeckUrl} onChange={(e) => setEditDeckUrl(e.target.value)} placeholder="URL du deck" />
|
|
<Textarea value={editDeckImporter} onChange={(e) => updateEditDeckInput(e.target.value)} placeholder="Deck List dans le format MTGO. Exemple : 1 Agate-Blade Assassin 1 Agate-Blade Assassin 1 Agate-Blade Assassin 1 Agate-Blade Assassin" className="min-h-48" />
|
|
<div><span className="font-bold">Commandant :</span> <span className="italic">{editDeckCommanderName}</span></div>
|
|
</div>
|
|
<DialogFooter>
|
|
<DialogClose>
|
|
<Button onClick={updateDeck} type="submit">Sauvegarder</Button>
|
|
</DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
|
|
{/* Delete Dialog */}
|
|
<AlertDialog>
|
|
<AlertDialogTrigger className="hidden" ref={deleteDialogRef}>Open</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Es-tu sûr·e?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Cette action ne peut être annulée et le deck sera effacé de nos serveurs.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => deleteAlertHandler("","set")}>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => deleteAlertHandler("","delete")}>Supprimer</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
|
|
{/* Separator */}
|
|
<div className="w-full h-0.5 rounded-md bg-stone-200"></div>
|
|
|
|
|
|
|
|
{/* Decks Display */}
|
|
<div className="flex flex-col gap-4 w-full items-center">
|
|
<Input className="text-3xl" placeholder="Rechercher des Decks..." onChange={(e) => filterDecks(e.target.value)}/>
|
|
{ loading && (
|
|
<Spinner className="mt-12" />
|
|
)}
|
|
{ !loading && (
|
|
<div className="w-full">
|
|
<Accordion type="single" collapsible className="w-full">
|
|
{ displayedDecks.map((deck) => (
|
|
<AccordionItem key={deck.id} value={deck.id}>
|
|
<AccordionTrigger trashFunction={() => deleteAlertHandler(deck.id, "set")} editFunction={() => editDialogHandler(deck.id)} className="text-3xl"><div className="flex flex-col"><span>{deck.name}</span></div></AccordionTrigger>
|
|
<a className="text-sm text-orange-500" target={deck.url ? "_blank" : ""} href={deck.url ? deck.url : "#"}>{deck.url ? deck.url : "No url"}</a>
|
|
<AccordionContent className="flex flex-col gap-4">
|
|
<MTGCardTextHover carte={deck.commander} className="text-lg" />
|
|
<div className="grid grid-flow-rows grid-cols-3 gap-2">
|
|
{ deck.cartes.map((carte_parent: cartes_dans_deckExtended) => (
|
|
<div key={carte_parent.carte.id} className="flex flex-row gap-2">
|
|
{carte_parent.amount}x <MTGCardTextHover carte={carte_parent.carte} className="text-stone-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
))}
|
|
</Accordion>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Toaster />
|
|
</div>
|
|
);
|
|
}
|