diff --git a/.gitignore b/.gitignore index 40f5492..bdfcac9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ app/next-env.d.ts # data app/tools/data/* +app/data/misc/* +app/data/commander/* +app/data/bset/* app/tools/json/* diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..3f9133c --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,26 @@ +node_modules/ +.env + +# testing +coverage + +# next.js +.next/ +out/ + +# production +build + +# debug +npm-debug.log* + +# typescript +*.tsbuildinfo +next-env.d.ts + +# data +tools/data +tools/json +data/misc/* +data/commander/* +data/bset/* diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..ddac9ee --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,28 @@ +FROM node:alpine AS build + +WORKDIR /app +COPY package.json ./ +# install dependencies +RUN npm install +COPY . . +# build +RUN npx prisma generate +RUN npm run build +# remove dev dependencies +RUN npm prune --production + +FROM node:alpine + +WORKDIR /app + +RUN apk add --no-cache openssl +# copy from build image +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/.next ./.next +COPY --from=build /app/public ./public +COPY --from=build /app/tools ./tools +COPY --from=build /app/data ./data + +EXPOSE 3000 +CMD ["npm","run","start"] diff --git a/app/app/api/json/[...slug]/route.ts b/app/app/api/json/[...slug]/route.ts new file mode 100644 index 0000000..241ca2d --- /dev/null +++ b/app/app/api/json/[...slug]/route.ts @@ -0,0 +1,25 @@ +import { NextResponse, NextRequest } from 'next/server' +import fs from "fs"; +import path from "path"; + +export async function GET(req: NextRequest, {params}: { params: { slug: string[] } }) { + try { + const jsonPath = await (params).slug.join("/") + const filePath = path.resolve(".",`data/${jsonPath}`); + const jsonBuffer = fs.readFileSync(filePath); + return new NextResponse(jsonBuffer,{ + status: 200, + headers: { + "content-type": "application/json", + } + }); + } catch (error) { + console.log(error) + return NextResponse.json( + { error: "Failed, check console" }, + { + status: 500, + } + ); + } +} diff --git a/app/app/bset/[bset]/page_content.tsx b/app/app/bset/[bset]/page_content.tsx index c6a70bd..1d8cea7 100644 --- a/app/app/bset/[bset]/page_content.tsx +++ b/app/app/bset/[bset]/page_content.tsx @@ -1,7 +1,8 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef } from 'react' import { CardGroup } from '@/components/ui/card-group' +import {PlaneswalkerIcon, SorceryIcon, InstantIcon, CreatureIcon, EnchantmentIcon, LandIcon, ArtifactIcon} from '@/components/ui/symbols-icons' interface PageContentProps { bset: string @@ -17,8 +18,11 @@ export default function PageContent({bset}: PageContentProps) { const [enchantmentList, setEnchantmentList] = useState([]) const [landList, setLandList] = useState([]) + const [scrollState, setScrollState] = useState("commander") + const CardListRef = useRef(null) + useEffect(() => { - fetch('http://localhost:8072/bset/'+bset+'.json').then((res) => { + fetch('/api/json/bset/'+bset+'.json').then((res) => { if(res.status == 200) { res.json().then((data) => { const limit = 20 @@ -34,17 +38,58 @@ export default function PageContent({bset}: PageContentProps) { }) } }) - }, []) + }, []) + + useEffect(() => { + const handleScroll = () => { + const windowHeight = window.innerHeight + const TOP_MARGIN = 0.1 + const BOTTOM_MARGIN = 0.2 + const card_children = CardListRef.current?.children + if (card_children) { + for (const child of card_children){ + const targetBounds = child.getBoundingClientRect() + if( targetBounds.bottom > windowHeight * TOP_MARGIN && targetBounds.top < windowHeight * ( 1 - BOTTOM_MARGIN ) ) { + setScrollState(child.id) + break + } + } + } + }; + + // Add event listener to the window + window.addEventListener('scroll', handleScroll); + + // Remove event listener when the component is unmounted + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + return ( -
- - - - - - - - + ); } diff --git a/app/app/bset/all/page.tsx b/app/app/bset/all/page.tsx index 48a0da6..1c03782 100644 --- a/app/app/bset/all/page.tsx +++ b/app/app/bset/all/page.tsx @@ -1,6 +1,7 @@ 'use client' import { useEffect, useState } from 'react' +import { Input } from '@/components/ui/input' interface bsetJsonObject { name: string, @@ -11,29 +12,45 @@ interface bsetJsonObject { export default function Home() { - const [BsetList, setBsetList] = useState([]) + const [displayedBsetList, setDisplayedBsetList] = useState([]) + const [originalBsetList, setOriginalBsetList] = useState([]) useEffect(() => { - fetch('http://localhost:8072/misc/bsets.json').then((res) => { + fetch('/api/json/misc/bsets.json').then((res) => { if(res.status == 200) { res.json().then((data) => { - setBsetList(data) + setOriginalBsetList(data) + setDisplayedBsetList(data) }) } }) }, []) + + function filterBsetList(searchString:string) { + if(searchString != "") { + setDisplayedBsetList(originalBsetList.filter((bset:bsetJsonObject) => bset.name.toLowerCase().includes(searchString.toLowerCase()))) + } else { + setDisplayedBsetList(originalBsetList) + } + } + return ( -
- { BsetList.map((bset: bsetJsonObject) => ( - -
- { bset.icons.map((icon) => ( - - ))} -
- {bset.name} -
- ))} +
+
+ filterBsetList(e.target.value)}/> +
+ { displayedBsetList.map((bset: bsetJsonObject) => ( + +
+ { bset.icons.map((icon) => ( + + ))} +
+ {bset.name} +
+ ))} +
+
); } diff --git a/app/app/commander/[color]/page_content.tsx b/app/app/commander/[color]/page_content.tsx index 120f2bf..d3da616 100644 --- a/app/app/commander/[color]/page_content.tsx +++ b/app/app/commander/[color]/page_content.tsx @@ -11,7 +11,7 @@ export default function PageContent({color}: PageContentProps) { const [commanderCardList, setCommanderCardList] = useState([]) useEffect(() => { - fetch('http://localhost:8072/commander/'+color+'.json').then((res) => { + fetch('/api/json/commander/'+color+'.json').then((res) => { if(res.status == 200) { res.json().then((data) => { const limit = 20 @@ -22,8 +22,10 @@ export default function PageContent({color}: PageContentProps) { }) }, []) return ( -
- +
+
+ +
); } diff --git a/app/app/commander/top/page.tsx b/app/app/commander/top/page.tsx index 7b61e02..633c993 100644 --- a/app/app/commander/top/page.tsx +++ b/app/app/commander/top/page.tsx @@ -7,7 +7,7 @@ export default function Home() { const [commanderCardList, setCommanderCardList] = useState([]) useEffect(() => { - fetch('http://localhost:8072/commander/top.json').then((res) => { + fetch('/api/json/commander/top.json').then((res) => { if(res.status == 200) { res.json().then((data) => { const limit = 20 @@ -18,8 +18,10 @@ export default function Home() { }) }, []) return ( -
- +
+
+ +
); } diff --git a/app/app/fonts/inter-tight-latin-400-italic.ttf b/app/app/fonts/inter-tight-latin-400-italic.ttf new file mode 100644 index 0000000..5c011df Binary files /dev/null and b/app/app/fonts/inter-tight-latin-400-italic.ttf differ diff --git a/app/app/fonts/inter-tight-latin-400-normal.ttf b/app/app/fonts/inter-tight-latin-400-normal.ttf new file mode 100644 index 0000000..4301e14 Binary files /dev/null and b/app/app/fonts/inter-tight-latin-400-normal.ttf differ diff --git a/app/app/fonts/inter-tight-latin-800-italic.ttf b/app/app/fonts/inter-tight-latin-800-italic.ttf new file mode 100644 index 0000000..de14075 Binary files /dev/null and b/app/app/fonts/inter-tight-latin-800-italic.ttf differ diff --git a/app/app/fonts/inter-tight-latin-800-normal.ttf b/app/app/fonts/inter-tight-latin-800-normal.ttf new file mode 100644 index 0000000..3b5ae8e Binary files /dev/null and b/app/app/fonts/inter-tight-latin-800-normal.ttf differ diff --git a/app/app/globals.css b/app/app/globals.css index c26cc9e..9e52d80 100644 --- a/app/app/globals.css +++ b/app/app/globals.css @@ -11,10 +11,38 @@ body { src: url("./fonts/Beleren2016-Bold.woff"); } +@font-face { + font-family: "Inter-Tight-Normal"; + src: url("./fonts/inter-tight-latin-400-normal.ttf"); +} + +@font-face { + font-family: "Inter-Tight-Italic"; + src: url("./fonts/inter-tight-latin-400-italic.ttf"); +} + +@font-face { + font-family: "Inter-Tight-Bold"; + src: url("./fonts/inter-tight-latin-800-normal.ttf"); +} + +@font-face { + font-family: "Inter-Tight-Bold-Italic"; + src: url("./fonts/inter-tight-latin-800-italic.ttf"); +} + .font-beleren { font-family: 'Beleren'; } +.font-inter-tight { + font-family: 'Inter-Tight-Normal'; +} + +.neutral-svg-filter { + filter: invert(49%) sepia(2%) saturate(1917%) hue-rotate(343deg) brightness(89%) contrast(85%); +} + @layer utilities { .text-balance { text-wrap: balance; diff --git a/app/app/layout.tsx b/app/app/layout.tsx index 69f3d8e..4c63ff4 100644 --- a/app/app/layout.tsx +++ b/app/app/layout.tsx @@ -38,10 +38,15 @@ export default async function RootLayout({ return ( - {children} +
+ {children} +
+
+

Brawlset is unofficial Fan Content permitted under the Fan Content Policy. Not approved/endorsed by Wizards. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC.

+
); diff --git a/app/app/page.tsx b/app/app/page.tsx index 16bd969..e4e48a0 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -2,7 +2,9 @@ export default function Home() { return ( <>
-

/!\ Work in Progress /!\

+

The BrawlSet

+

Un système de règles MTG basé sur le mode de jeu commander et inventé à Rennes, pays de la galette saucisse.

+Pour plus d'informations allez voir les règles ou la FAQ.

); diff --git a/app/components/ui/card-group.tsx b/app/components/ui/card-group.tsx index ba5ea52..b72cce9 100644 --- a/app/components/ui/card-group.tsx +++ b/app/components/ui/card-group.tsx @@ -21,18 +21,22 @@ interface CardGroupProps { groupName: string, cards: carte_from_stats[], showPrice?: boolean, - showStats?: boolean + showStats?: boolean, + id?: string, + Icon?: any } -const CardGroup = ({ className, groupName, cards, showPrice=true, showStats=true}: CardGroupProps) => { +const CardGroup = ({ className, groupName, cards, showPrice=true, showStats=true, id, Icon}: CardGroupProps) => { return ( -
-

{groupName}

-
+
+ { Icon && ( + )} +

{groupName}

+
+
{cards.map((card: carte_from_stats) => ( diff --git a/app/components/ui/mtg-card.tsx b/app/components/ui/mtg-card.tsx index 3317881..a01a02e 100644 --- a/app/components/ui/mtg-card.tsx +++ b/app/components/ui/mtg-card.tsx @@ -31,12 +31,12 @@ const MTGCard = ({ className, imageURI, cardname, url, nbrDecks, totalDecks, per {setLoaded(true)}} loading="lazy" />
{ price != undefined && ( - {price}€ + {price}€ )} - {cardname} + {cardname} { nbrDecks != undefined && ( <> - {nbrDecks} de {totalDecks} Decks ({percentDecks}%) + {nbrDecks} Deck{nbrDecks > 1 ? "s" : ""} sur {totalDecks} ({percentDecks}%) )}
diff --git a/app/components/ui/navigation-bar.tsx b/app/components/ui/navigation-bar.tsx index 37fedd3..88ffe8f 100644 --- a/app/components/ui/navigation-bar.tsx +++ b/app/components/ui/navigation-bar.tsx @@ -17,6 +17,7 @@ import { import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { IconUserFilled } from "@tabler/icons-react" +import { IconChevronDown } from "@tabler/icons-react" import { Black, Blue, Green, White, Red, Colorless } from "@/components/ui/mana-icons" import { useEffect, useState } from 'react' @@ -37,7 +38,7 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { const [bsetsList, setBsetsList] = useState([]) useEffect(() => { - fetch('http://localhost:8072/misc/bsets.json').then((res) => { + fetch('/api/json/misc/bsets.json').then((res) => { if(res.status == 200) { res.json().then((data) => { setBsetsList(data) @@ -49,15 +50,17 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { return ( -
+
+
- + Commandants @@ -117,71 +120,91 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { - - +
+ + +
Azorius
- - +
+ + +
Dimir
- - +
+ + +
Rakdos
- - +
+ + +
Gruul
- - +
+ + +
Selesnya
- - +
+ + +
Orzhov
- - +
+ + +
Izzet
- - +
+ + +
Golgari
- - +
+ + +
Boros
- - +
+ + +
Simic
@@ -196,81 +219,101 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { - - - +
+ + + +
Esper
- - - +
+ + + +
Grixis
- - - +
+ + + +
Jund
- - - +
+ + + +
Naya
- - - +
+ + + +
Bant
- - - +
+ + + +
Abzan
- - - +
+ + + +
Jeskai
- - - +
+ + + +
Sultai
- - - +
+ + + +
Mardu
- - - +
+ + + +
Temur
@@ -285,56 +328,68 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { - - - - +
+ + + + +
Yore-Tiller
- - - - +
+ + + + +
Glint-Eye
- - - - +
+ + + + +
Dune-Brood
- - - - +
+ + + + +
Ink-Treader
- - - - +
+ + + + +
Witch-Maw
- - - - - +
+ + + + + +
5 couleurs
@@ -346,7 +401,7 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) {
- + BSets @@ -373,23 +428,23 @@ export function NavigationBar ({ isLoggedIn, username}: NavigationProps) { - + Règles + FAQ
- { !isLoggedIn && <> - - + Connexion + } { isLoggedIn && <> - Decks + Decks - - {username} + + {username} } diff --git a/app/components/ui/symbols-icons.tsx b/app/components/ui/symbols-icons.tsx new file mode 100644 index 0000000..a39a235 --- /dev/null +++ b/app/components/ui/symbols-icons.tsx @@ -0,0 +1,49 @@ +import { cn } from "@/lib/utils" + +interface SymbolsIconProps { + className: string +} + +const PlaneswalkerIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +PlaneswalkerIcon.displayName = "PlaneswalkerIcon" + +const SorceryIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +SorceryIcon.displayName = "SorceryIcon" + +const InstantIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +InstantIcon.displayName = "InstantIcon" + +const CreatureIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +CreatureIcon.displayName = "CreatureIcon" + +const ArtifactIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +ArtifactIcon.displayName = "ArtifactIcon" + +const EnchantmentIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +EnchantmentIcon.displayName = "EnchantmentIcon" + +const LandIcon = ({ className }: SymbolsIconProps) => { + return ( + + )} +LandIcon.displayName = "LandIcon" + +export { PlaneswalkerIcon, SorceryIcon, InstantIcon, CreatureIcon, ArtifactIcon, EnchantmentIcon, LandIcon } diff --git a/app/docker-compose.yml b/app/docker-compose.yml deleted file mode 100644 index 6a5cf5b..0000000 --- a/app/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - nginx: - container_name: json_files - image: nginx - ports: - - 8072:80 - volumes: - - ./tools/json:/usr/share/nginx/html:ro - - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - restart: unless-stopped diff --git a/app/nginx.conf b/app/nginx.conf deleted file mode 100644 index dc08c25..0000000 --- a/app/nginx.conf +++ /dev/null @@ -1,56 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name localhost; - - #access_log /var/log/nginx/host.access.log main; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - # - # Custom headers and headers various browsers *should* be OK with but aren't - # - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; - # - # Tell client that this pre-flight info is valid for 20 days - # - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain; charset=utf-8'; - add_header 'Content-Length' 0; - return 204; - } - if ($request_method = 'POST') { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - } - if ($request_method = 'GET') { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - } - } - - error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} -} diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 7444167..9665e14 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -1,6 +1,7 @@ generator client { provider = "prisma-client-js" previewFeatures = ["relationJoins"] + binaryTargets = ["native","linux-musl","linux-musl-openssl-3.0.x"] } datasource db { diff --git a/app/public/assets/artifact.svg b/app/public/assets/artifact.svg new file mode 100644 index 0000000..1590661 --- /dev/null +++ b/app/public/assets/artifact.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/public/assets/creature.svg b/app/public/assets/creature.svg new file mode 100644 index 0000000..cef6b0e --- /dev/null +++ b/app/public/assets/creature.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/public/assets/enchantment.svg b/app/public/assets/enchantment.svg new file mode 100644 index 0000000..4935ddf --- /dev/null +++ b/app/public/assets/enchantment.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/public/assets/instant.svg b/app/public/assets/instant.svg new file mode 100644 index 0000000..3316ad0 --- /dev/null +++ b/app/public/assets/instant.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/public/assets/land.svg b/app/public/assets/land.svg new file mode 100644 index 0000000..d5ecc9f --- /dev/null +++ b/app/public/assets/land.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/public/assets/planeswalker.svg b/app/public/assets/planeswalker.svg new file mode 100644 index 0000000..7e3851c --- /dev/null +++ b/app/public/assets/planeswalker.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/app/public/assets/sorcery.svg b/app/public/assets/sorcery.svg new file mode 100644 index 0000000..f7a2e16 --- /dev/null +++ b/app/public/assets/sorcery.svg @@ -0,0 +1,38 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/tools/createJson.mjs b/app/tools/createJson.mjs index 095274c..d8bbfbc 100644 --- a/app/tools/createJson.mjs +++ b/app/tools/createJson.mjs @@ -65,6 +65,7 @@ function getColorName(colorArray) { async function createJson() { console.log("Fetching data...") + console.log(process.env.NODE_ENV) const bsets = await db.bset.findMany({ relationLoadStrategy: "join", include: { @@ -165,7 +166,12 @@ async function createJson() { } }) - writeFileSync(import.meta.dirname + "/json/misc/bsets.json",JSON.stringify(bsets_list_export), 'utf8') + if(process.env.NODE_ENV == "production") { + console.log('Production detected !') + writeFileSync("/app/data/misc/bsets.json",JSON.stringify(bsets_list_export), 'utf8') + } else { + writeFileSync(import.meta.dirname + "/json/misc/bsets.json",JSON.stringify(bsets_list_export), 'utf8') + } //for (const card of all_cards) { @@ -173,7 +179,12 @@ async function createJson() { for (const index of Object.keys(commanderData)) { let JSONToWrite = commanderData[index].sort((a,b) => b.percent_decks - a.percent_decks) - writeFileSync(import.meta.dirname + "/json/commander/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + if(process.env.NODE_ENV == "production") { + console.log('Production detected !') + writeFileSync("/app/data/commander/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + } else { + writeFileSync(import.meta.dirname + "/json/commander/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + } } for (const index of Object.keys(bset_cards_data_export)) { @@ -181,7 +192,12 @@ async function createJson() { for (const type of Object.keys(JSONToWrite)) { JSONToWrite[type] = JSONToWrite[type].sort((a,b) => b.percent_decks - a.percent_decks) } - writeFileSync(import.meta.dirname + "/json/bset/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + if(process.env.NODE_ENV == "production") { + console.log('Production detected !') + writeFileSync("/app/data/bset/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + } else { + writeFileSync(import.meta.dirname + "/json/bset/" + index + ".json",JSON.stringify(JSONToWrite), 'utf8') + } } } diff --git a/app/tsconfig.json b/app/tsconfig.json index e7ff90f..f59b9cf 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es2023", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,