brawlset/app/app/account/profile/decks/page.tsx
2025-02-12 15:31:51 +01:00

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.&#10;&#10;Exemple :&#10;&#10;1 Agate-Blade Assassin&#10;1 Agate-Blade Assassin&#10;1 Agate-Blade Assassin&#10;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.&#10;&#10;Exemple :&#10;&#10;1 Agate-Blade Assassin&#10;1 Agate-Blade Assassin&#10;1 Agate-Blade Assassin&#10;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>
);
}