feat: implement daily character guessing game with local storage and hint system
- Added character selection and history management using local storage. - Implemented hint system that unlocks based on the number of guesses. - Enhanced UI with animations for hint unlocks and special win conditions. - Created a server endpoint to record wins in the database.
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.svelte-kit
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
local.db
|
||||||
|
drizzle/meta
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
/.vscode
|
||||||
|
|
||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
|
|||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM node:24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=builder /app ./
|
||||||
|
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1000 node && adduser -D -u 1000 -G node node
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 4173
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
130
SCRAPER.md
130
SCRAPER.md
@@ -1,130 +0,0 @@
|
|||||||
# One Piece Scraper
|
|
||||||
|
|
||||||
Script pour scraper les données des personnages de One Piece depuis le fandom wiki français.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Installe les dépendances d'abord :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Utilisation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Scraper tous les formats (JSON, CSV, SQL)
|
|
||||||
npm run scrape
|
|
||||||
|
|
||||||
# Ou spécifier un format
|
|
||||||
node scripts/scrape-onepiece.js json # JSON uniquement
|
|
||||||
node scripts/scrape-onepiece.js csv # CSV uniquement
|
|
||||||
node scripts/scrape-onepiece.js sql # SQL uniquement
|
|
||||||
node scripts/scrape-onepiece.js all # Tous les formats (défaut)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sortie
|
|
||||||
|
|
||||||
Les données seront sauvegardées dans le dossier `scraped-data/` :
|
|
||||||
|
|
||||||
- **characters.json** - Format JSON avec toutes les données structurées
|
|
||||||
- **characters.csv** - Format CSV pour importer dans Excel/Sheets
|
|
||||||
- **characters.sql** - Statements SQL avec gestion des conflits (upsert)
|
|
||||||
|
|
||||||
## Données extraites
|
|
||||||
|
|
||||||
Pour chaque personnage :
|
|
||||||
- 📝 **Nom** - Nom du personnage
|
|
||||||
- 👤 **Genre** - Masculin/Féminin (extrait des catégories)
|
|
||||||
- 🎂 **Âge** - Âge le plus récent (post-ellipse), chiffres uniquement
|
|
||||||
- 📏 **Taille** - Normalisée en mètres (format: "2.74" ou "174" pour cm)
|
|
||||||
- 🌍 **Origine** - Lieu d'origine (sans parenthèses)
|
|
||||||
- 😈 **Fruit du Démon** - Nom du fruit (si applicable)
|
|
||||||
- 👥 **Affiliations** - Liste des affiliations (équipages, organisations)
|
|
||||||
- 💰 **Prime** - Bounty la plus récente
|
|
||||||
- ⚡ **Haki** - Liste des types de Haki (Observation, Armament, Conqueror)
|
|
||||||
- 📖 **Première Apparition** - Numéro de chapitre
|
|
||||||
- 🖼️ **Image** - URL de l'image portrait nettoyée
|
|
||||||
- 🔗 **Fandom URL** - Lien vers la page wiki
|
|
||||||
|
|
||||||
## Personnages scrapés
|
|
||||||
|
|
||||||
Le script scrape tous les personnages canon de la liste officielle du Fandom wiki français.
|
|
||||||
|
|
||||||
**Personnages actuellement filtrés** : Luffy et Moria (modifiable dans `fetchAllCharactersUrl`)
|
|
||||||
|
|
||||||
Pour scraper tous les personnages, retire le filtre dans la fonction `fetchAllCharactersUrl`.
|
|
||||||
|
|
||||||
## Fonctionnalités avancées
|
|
||||||
|
|
||||||
### Requêtes parallèles
|
|
||||||
- Le scraper traite 5 personnages en parallèle pour plus d'efficacité
|
|
||||||
- Concurrency configurable dans le code
|
|
||||||
|
|
||||||
### Nettoyage des données
|
|
||||||
- Suppression automatique des citations/références (`<sup>` tags)
|
|
||||||
- Âge : extraction du dernier âge (après ellipse), sans parenthèses
|
|
||||||
- Taille : normalisation m/cm et suppression des parenthèses
|
|
||||||
- Origine : suppression du contenu entre parenthèses
|
|
||||||
- Image : sélection automatique du portrait
|
|
||||||
- Première apparition : extraction du numéro de chapitre uniquement
|
|
||||||
|
|
||||||
### SQL Upsert
|
|
||||||
- Le SQL généré utilise `INSERT ... ON CONFLICT(name) DO UPDATE`
|
|
||||||
- Met à jour les personnages existants au lieu de créer des doublons
|
|
||||||
- Compatible SQLite (utilisé par le projet)
|
|
||||||
|
|
||||||
### Formats de données
|
|
||||||
- **Haki** : Stocké comme array JSON dans SQL : `["Observation","Armament"]`
|
|
||||||
- **Affiliations** : Liste dans JSON, comma-separated dans CSV/SQL
|
|
||||||
- Tous les champs nullable supportent `NULL` dans SQL
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Modifier les personnages filtrés
|
|
||||||
|
|
||||||
Dans `scripts/scrape-onepiece.js`, fonction `fetchAllCharactersUrl` :
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Filtrer pour des personnages spécifiques
|
|
||||||
if (nameLower.includes('luffy') || nameLower.includes('moria')) {
|
|
||||||
characters.push({ name: charName, url: charLink });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ajuster la concurrence
|
|
||||||
|
|
||||||
Dans la fonction `main` :
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const concurrency = 5; // Nombre de requêtes simultanées
|
|
||||||
```
|
|
||||||
|
|
||||||
## Importer les données SQL
|
|
||||||
|
|
||||||
Après avoir généré le fichier SQL, importe-le dans la base de données :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run db:import
|
|
||||||
```
|
|
||||||
|
|
||||||
Ce script :
|
|
||||||
- Lit automatiquement `scraped-data/characters.sql`
|
|
||||||
- Exécute chaque statement individuellement
|
|
||||||
- Affiche une barre de progression
|
|
||||||
- Gère les erreurs sans bloquer l'import complet
|
|
||||||
- Utilise le upsert pour éviter les doublons
|
|
||||||
|
|
||||||
**Note** : Assure-toi d'avoir exécuté les migrations avant l'import :
|
|
||||||
```bash
|
|
||||||
npm run db:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes techniques
|
|
||||||
|
|
||||||
- Source : `https://onepiece.fandom.com/fr/wiki`
|
|
||||||
- Parseur : Cheerio (DOM parsing)
|
|
||||||
- Traitement parallèle avec `Promise.all`
|
|
||||||
- User-Agent configuré pour éviter les blocages
|
|
||||||
- Pas de délai entre requêtes (batches parallèles)
|
|
||||||
- Gestion d'erreurs par personnage (ne bloque pas le scraping complet)
|
|
||||||
9
docker-entrypoint.sh
Normal file
9
docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Migrate the database
|
||||||
|
npm run db:migrate
|
||||||
|
|
||||||
|
# Start the production server
|
||||||
|
exec npm run preview --host 0.0.0.0
|
||||||
|
|
||||||
@@ -1,32 +1,103 @@
|
|||||||
|
CREATE TABLE `arc` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`startChapter` integer NOT NULL,
|
||||||
|
`endChapter` integer,
|
||||||
|
`url` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE `character` (
|
CREATE TABLE `character` (
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
`gender` text,
|
`gender` text,
|
||||||
`age` integer,
|
`age` integer,
|
||||||
`affiliations` text,
|
`affiliations` text,
|
||||||
`devilFruit` text,
|
`devilFruitId` text,
|
||||||
`haki` text,
|
`hakiObservation` integer DEFAULT false,
|
||||||
`bounty` integer,
|
`hakiArmament` integer DEFAULT false,
|
||||||
|
`hakiConqueror` integer DEFAULT false,
|
||||||
|
`bounty` integer DEFAULT 0,
|
||||||
`height` real,
|
`height` real,
|
||||||
`origin` text,
|
`origin` text,
|
||||||
`firstAppearance` text,
|
`firstAppearance` integer NOT NULL,
|
||||||
`pictureUrl` text,
|
`pictureUrl` text,
|
||||||
FOREIGN KEY (`devilFruit`) REFERENCES `devilFruit`(`id`) ON UPDATE no action ON DELETE no action
|
`epithets` text,
|
||||||
|
`status` text,
|
||||||
|
`arcId` text,
|
||||||
|
`url` text,
|
||||||
|
`isInDailyMode` integer DEFAULT true,
|
||||||
|
FOREIGN KEY (`devilFruitId`) REFERENCES `devilFruit`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`arcId`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE `characterHistory` (
|
CREATE TABLE `characterHistory` (
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
`characterId` text,
|
`characterId` text,
|
||||||
`date` integer,
|
`date` text,
|
||||||
|
`won` integer DEFAULT 0 NOT NULL,
|
||||||
`createdAt` integer NOT NULL,
|
`createdAt` integer NOT NULL,
|
||||||
`updatedAt` integer NOT NULL,
|
`updatedAt` integer NOT NULL,
|
||||||
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
|
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `characterOverride` (
|
||||||
|
`characterId` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
`gender` text,
|
||||||
|
`age` integer,
|
||||||
|
`affiliations` text,
|
||||||
|
`devilFruitId` text,
|
||||||
|
`hakiObservation` integer,
|
||||||
|
`hakiArmament` integer,
|
||||||
|
`hakiConqueror` integer,
|
||||||
|
`bounty` integer,
|
||||||
|
`height` real,
|
||||||
|
`origin` text,
|
||||||
|
`firstAppearance` integer NOT NULL,
|
||||||
|
`pictureUrl` text,
|
||||||
|
`epithets` text,
|
||||||
|
`status` text,
|
||||||
|
`arcId` text,
|
||||||
|
`url` text,
|
||||||
|
`notes` text,
|
||||||
|
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`devilFruitId`) REFERENCES `devilFruit`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`arcId`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `characterScrapeValidation` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`gender` text,
|
||||||
|
`age` integer,
|
||||||
|
`affiliations` text,
|
||||||
|
`devilFruitId` text,
|
||||||
|
`hakiObservation` integer DEFAULT false,
|
||||||
|
`hakiArmament` integer DEFAULT false,
|
||||||
|
`hakiConqueror` integer DEFAULT false,
|
||||||
|
`bounty` integer,
|
||||||
|
`height` real,
|
||||||
|
`origin` text,
|
||||||
|
`firstAppearance` integer NOT NULL,
|
||||||
|
`pictureUrl` text,
|
||||||
|
`epithets` text,
|
||||||
|
`status` text,
|
||||||
|
`arcId` text,
|
||||||
|
`url` text,
|
||||||
|
FOREIGN KEY (`devilFruitId`) REFERENCES `devilFruit`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`arcId`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `config` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE `devilFruit` (
|
CREATE TABLE `devilFruit` (
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
`type` text
|
`type` text,
|
||||||
|
`url` text
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE UNIQUE INDEX `devilFruit_name_unique` ON `devilFruit` (`name`);--> statement-breakpoint
|
CREATE UNIQUE INDEX `devilFruit_name_unique` ON `devilFruit` (`name`);--> statement-breakpoint
|
||||||
@@ -1,9 +1,54 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "40edd98b-5a47-4a5e-a5b8-8a0f6eaaec76",
|
"id": "d1237d76-8f1c-4721-b8dd-d31082ed7b9a",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
|
"arc": {
|
||||||
|
"name": "arc",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"startChapter": {
|
||||||
|
"name": "startChapter",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"endChapter": {
|
||||||
|
"name": "endChapter",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
"character": {
|
"character": {
|
||||||
"name": "character",
|
"name": "character",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -42,26 +87,44 @@
|
|||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"devilFruit": {
|
"devilFruitId": {
|
||||||
"name": "devilFruit",
|
"name": "devilFruitId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"haki": {
|
"hakiObservation": {
|
||||||
"name": "haki",
|
"name": "hakiObservation",
|
||||||
"type": "text",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"hakiArmament": {
|
||||||
|
"name": "hakiArmament",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"hakiConqueror": {
|
||||||
|
"name": "hakiConqueror",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"bounty": {
|
"bounty": {
|
||||||
"name": "bounty",
|
"name": "bounty",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
},
|
},
|
||||||
"height": {
|
"height": {
|
||||||
"name": "height",
|
"name": "height",
|
||||||
@@ -79,9 +142,9 @@
|
|||||||
},
|
},
|
||||||
"firstAppearance": {
|
"firstAppearance": {
|
||||||
"name": "firstAppearance",
|
"name": "firstAppearance",
|
||||||
"type": "text",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"pictureUrl": {
|
"pictureUrl": {
|
||||||
@@ -90,16 +153,65 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"epithets": {
|
||||||
|
"name": "epithets",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"arcId": {
|
||||||
|
"name": "arcId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"isInDailyMode": {
|
||||||
|
"name": "isInDailyMode",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"character_devilFruit_devilFruit_id_fk": {
|
"character_devilFruitId_devilFruit_id_fk": {
|
||||||
"name": "character_devilFruit_devilFruit_id_fk",
|
"name": "character_devilFruitId_devilFruit_id_fk",
|
||||||
"tableFrom": "character",
|
"tableFrom": "character",
|
||||||
"tableTo": "devilFruit",
|
"tableTo": "devilFruit",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"devilFruit"
|
"devilFruitId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"character_arcId_arc_id_fk": {
|
||||||
|
"name": "character_arcId_arc_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "arc",
|
||||||
|
"columnsFrom": [
|
||||||
|
"arcId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"id"
|
"id"
|
||||||
@@ -131,11 +243,19 @@
|
|||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
"name": "date",
|
"name": "date",
|
||||||
"type": "integer",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"won": {
|
||||||
|
"name": "won",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -171,6 +291,379 @@
|
|||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"checkConstraints": {}
|
"checkConstraints": {}
|
||||||
},
|
},
|
||||||
|
"characterOverride": {
|
||||||
|
"name": "characterOverride",
|
||||||
|
"columns": {
|
||||||
|
"characterId": {
|
||||||
|
"name": "characterId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"name": "gender",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"name": "age",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"affiliations": {
|
||||||
|
"name": "affiliations",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"devilFruitId": {
|
||||||
|
"name": "devilFruitId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hakiObservation": {
|
||||||
|
"name": "hakiObservation",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hakiArmament": {
|
||||||
|
"name": "hakiArmament",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hakiConqueror": {
|
||||||
|
"name": "hakiConqueror",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"bounty": {
|
||||||
|
"name": "bounty",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"name": "origin",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"firstAppearance": {
|
||||||
|
"name": "firstAppearance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pictureUrl": {
|
||||||
|
"name": "pictureUrl",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"epithets": {
|
||||||
|
"name": "epithets",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"arcId": {
|
||||||
|
"name": "arcId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"characterOverride_characterId_character_id_fk": {
|
||||||
|
"name": "characterOverride_characterId_character_id_fk",
|
||||||
|
"tableFrom": "characterOverride",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"characterId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"characterOverride_devilFruitId_devilFruit_id_fk": {
|
||||||
|
"name": "characterOverride_devilFruitId_devilFruit_id_fk",
|
||||||
|
"tableFrom": "characterOverride",
|
||||||
|
"tableTo": "devilFruit",
|
||||||
|
"columnsFrom": [
|
||||||
|
"devilFruitId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"characterOverride_arcId_arc_id_fk": {
|
||||||
|
"name": "characterOverride_arcId_arc_id_fk",
|
||||||
|
"tableFrom": "characterOverride",
|
||||||
|
"tableTo": "arc",
|
||||||
|
"columnsFrom": [
|
||||||
|
"arcId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"characterScrapeValidation": {
|
||||||
|
"name": "characterScrapeValidation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"name": "gender",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"name": "age",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"affiliations": {
|
||||||
|
"name": "affiliations",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"devilFruitId": {
|
||||||
|
"name": "devilFruitId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hakiObservation": {
|
||||||
|
"name": "hakiObservation",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"hakiArmament": {
|
||||||
|
"name": "hakiArmament",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"hakiConqueror": {
|
||||||
|
"name": "hakiConqueror",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"bounty": {
|
||||||
|
"name": "bounty",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"name": "origin",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"firstAppearance": {
|
||||||
|
"name": "firstAppearance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pictureUrl": {
|
||||||
|
"name": "pictureUrl",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"epithets": {
|
||||||
|
"name": "epithets",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"arcId": {
|
||||||
|
"name": "arcId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"characterScrapeValidation_devilFruitId_devilFruit_id_fk": {
|
||||||
|
"name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk",
|
||||||
|
"tableFrom": "characterScrapeValidation",
|
||||||
|
"tableTo": "devilFruit",
|
||||||
|
"columnsFrom": [
|
||||||
|
"devilFruitId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"characterScrapeValidation_arcId_arc_id_fk": {
|
||||||
|
"name": "characterScrapeValidation_arcId_arc_id_fk",
|
||||||
|
"tableFrom": "characterScrapeValidation",
|
||||||
|
"tableTo": "arc",
|
||||||
|
"columnsFrom": [
|
||||||
|
"arcId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"name": "config",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
"devilFruit": {
|
"devilFruit": {
|
||||||
"name": "devilFruit",
|
"name": "devilFruit",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -194,6 +687,13 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1772148571269,
|
"when": 1772325597983,
|
||||||
"tag": "0000_dapper_sage",
|
"tag": "0000_graceful_master_mold",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate && npx tsx scripts/init-column-config.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:import": "node scripts/import-sql.js",
|
"db:import": "npx tsx scripts/import-json.ts",
|
||||||
|
"db:set-daily-mode": "npx tsx scripts/set-daily-mode.ts",
|
||||||
"auth:schema": "npx @better-auth/cli generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
"auth:schema": "npx @better-auth/cli generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
||||||
"scrape": "node scripts/scrape-onepiece.js"
|
"scrape": "node scripts/scrape-onepiece.js"
|
||||||
},
|
},
|
||||||
|
|||||||
145
scripts/daily-characters.json
Normal file
145
scripts/daily-characters.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
[
|
||||||
|
"aladdin_aladdin",
|
||||||
|
"alvida_alvida",
|
||||||
|
"aramaki_aramaki",
|
||||||
|
"arlong_arlong",
|
||||||
|
"ashura_doji_ashura_doji",
|
||||||
|
"baby_5_baby_5",
|
||||||
|
"baggy_baggy",
|
||||||
|
"bartholomew_kuma_bartholomew_kuma",
|
||||||
|
"bartolomeo_bartolomeo",
|
||||||
|
"basil_hawkins_basil_hawkins",
|
||||||
|
"batman_batman",
|
||||||
|
"bellamy_bellamy",
|
||||||
|
"belo_betty_belo_betty",
|
||||||
|
"ben_beckman_ben_beckman",
|
||||||
|
"bentham_bentham",
|
||||||
|
"bepo_bepo",
|
||||||
|
"black_maria_black_maria",
|
||||||
|
"boa_hancock_boa_hancock",
|
||||||
|
"boa_marigold_boa_marigold",
|
||||||
|
"boa_sandersonia_boa_sandersonia",
|
||||||
|
"borsalino_borsalino",
|
||||||
|
"brogy_brogy",
|
||||||
|
"brook_brook",
|
||||||
|
"camie_camie",
|
||||||
|
"capone_bege_capone_bege",
|
||||||
|
"caribou_caribou",
|
||||||
|
"carrot_carrot",
|
||||||
|
"catarina_devon_catarina_devon",
|
||||||
|
"cavendish_cavendish",
|
||||||
|
"cesar_clown_cesar_clown",
|
||||||
|
"chinjao_chinjao",
|
||||||
|
"coby_coby",
|
||||||
|
"corazon_corazon",
|
||||||
|
"crocodile_crocodile",
|
||||||
|
"crocus_crocus",
|
||||||
|
"curly_dadan_curly_dadan",
|
||||||
|
"dalton_dalton",
|
||||||
|
"daz_bones_daz_bones",
|
||||||
|
"denjiro_denjiro",
|
||||||
|
"diamante_diamante",
|
||||||
|
"doc_q_doc_q",
|
||||||
|
"don_quichotte_doflamingo_don_quichotte_doflamingo",
|
||||||
|
"don_quichotte_rossinante_don_quichotte_rossinante",
|
||||||
|
"dorry_dorry",
|
||||||
|
"dracule_mihawk_dracule_mihawk",
|
||||||
|
"duval_duval",
|
||||||
|
"edward_newgate_edward_newgate",
|
||||||
|
"edward_weevil_edward_weevil",
|
||||||
|
"emporio_ivankov_emporio_ivankov",
|
||||||
|
"enel_enel",
|
||||||
|
"eustass_kid_eustass_kid",
|
||||||
|
"fisher_tiger_fisher_tiger",
|
||||||
|
"foxy_foxy",
|
||||||
|
"franky_franky",
|
||||||
|
"fujitora_fujitora",
|
||||||
|
"gan_forr_gan_forr",
|
||||||
|
"gecko_moria_gecko_moria",
|
||||||
|
"gin_gin",
|
||||||
|
"gol_d_roger_gol_d_roger",
|
||||||
|
"haguar_d_sauro_haguar_d_sauro",
|
||||||
|
"hajrudin_hajrudin",
|
||||||
|
"hannyabal_hannyabal",
|
||||||
|
"hatchan_hatchan",
|
||||||
|
"hina_hina",
|
||||||
|
"hody_jones_hody_jones",
|
||||||
|
"hyogoro_hyogoro",
|
||||||
|
"iceburg_iceburg",
|
||||||
|
"imu_imu",
|
||||||
|
"inazuma_inazuma",
|
||||||
|
"inuarashi_inuarashi",
|
||||||
|
"issho_issho",
|
||||||
|
"izo_izo",
|
||||||
|
"jabra_jabra",
|
||||||
|
"jack_jack",
|
||||||
|
"jesus_burgess_jesus_burgess",
|
||||||
|
"jewelry_bonney_jewelry_bonney",
|
||||||
|
"jinbei_jinbei",
|
||||||
|
"joy_boy_joy_boy",
|
||||||
|
"kaidou_kaidou",
|
||||||
|
"kaku_kaku",
|
||||||
|
"kalgara_kalgara",
|
||||||
|
"kalifa_kalifa",
|
||||||
|
"karasu_karasu",
|
||||||
|
"karoo_karoo",
|
||||||
|
"kawamatsu_kawamatsu",
|
||||||
|
"kaya_kaya",
|
||||||
|
"killer_killer",
|
||||||
|
"kinemon_kinemon",
|
||||||
|
"koala_koala",
|
||||||
|
"koby_koby",
|
||||||
|
"kong_kong",
|
||||||
|
"kozuki_hiyori_kozuki_hiyori",
|
||||||
|
"kozuki_momonosuke_kozuki_momonosuke",
|
||||||
|
"kozuki_oden_kozuki_oden",
|
||||||
|
"krieg_krieg",
|
||||||
|
"kureha_kureha",
|
||||||
|
"kuro_kuro",
|
||||||
|
"kurozumi_orochi_kurozumi_orochi",
|
||||||
|
"kuzan_kuzan",
|
||||||
|
"kyros_kyros",
|
||||||
|
"laboon_laboon",
|
||||||
|
"laffitte_laffitte",
|
||||||
|
"lao_g_lao_g",
|
||||||
|
"leo_leo",
|
||||||
|
"lindbergh_lindbergh",
|
||||||
|
"loki_loki",
|
||||||
|
"lucky_roux_lucky_roux",
|
||||||
|
"magellan_magellan",
|
||||||
|
"makino_makino",
|
||||||
|
"marco_marco",
|
||||||
|
"marshall_d_teach_marshall_d_teach",
|
||||||
|
"monkey_d_dragon_monkey_d_dragon",
|
||||||
|
"monkey_d_garp_monkey_d_garp",
|
||||||
|
"monkey_d_luffy_monkey_d_luffy",
|
||||||
|
"montblanc_norland_montblanc_norland",
|
||||||
|
"morgans_morgans",
|
||||||
|
"morley_morley",
|
||||||
|
"mr_3_mr_3",
|
||||||
|
"nami_nami",
|
||||||
|
"nefertari_cobra_nefertari_cobra",
|
||||||
|
"nefertari_vivi_nefertari_vivi",
|
||||||
|
"nekomamushi_nekomamushi",
|
||||||
|
"neptune_neptune",
|
||||||
|
"nico_robin_nico_robin",
|
||||||
|
"oars_oars",
|
||||||
|
"orlumbus_orlumbus",
|
||||||
|
"otohime_otohime",
|
||||||
|
"page_one_page_one",
|
||||||
|
"pandaman_pandaman",
|
||||||
|
"pekoms_pekoms",
|
||||||
|
"pell_pell",
|
||||||
|
"perona_perona",
|
||||||
|
"pica_pica",
|
||||||
|
"portgas_d_ace_portgas_d_ace",
|
||||||
|
"queen_queen",
|
||||||
|
"raizo_raizo",
|
||||||
|
"rebecca_rebecca",
|
||||||
|
"rob_lucci_rob_lucci",
|
||||||
|
"rocks_d_xebec_rocks_d_xebec",
|
||||||
|
"roronoa_zoro_roronoa_zoro",
|
||||||
|
"sabo_sabo",
|
||||||
|
"vegapunk_vegapunk",
|
||||||
|
"yamato_yamato"
|
||||||
|
]
|
||||||
406
scripts/import-json.ts
Normal file
406
scripts/import-json.ts
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { sql, eq } from 'drizzle-orm';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
|
||||||
|
|
||||||
|
type ArcRecord = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startChapter: number;
|
||||||
|
endChapter?: number | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DevilFruitRecord = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type?: DevilFruitType | string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CharacterRecord = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
gender?: string | null;
|
||||||
|
age?: number | null;
|
||||||
|
affiliations?: string[] | string | null;
|
||||||
|
devilFruitId?: string | null;
|
||||||
|
hakiObservation?: boolean;
|
||||||
|
hakiArmament?: boolean;
|
||||||
|
hakiConqueror?: boolean;
|
||||||
|
bounty?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
origin?: string | null;
|
||||||
|
firstAppearance?: number;
|
||||||
|
pictureUrl?: string | null;
|
||||||
|
epithets?: string[] | string | null;
|
||||||
|
status?: string | null;
|
||||||
|
arcId?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
|
||||||
|
|
||||||
|
const client = createClient({ url: DATABASE_URL });
|
||||||
|
const db = drizzle(client);
|
||||||
|
|
||||||
|
function readJsonFile<T>(path: string): T[] | null {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(path, 'utf-8');
|
||||||
|
return JSON.parse(content) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullable<T>(value: T | undefined | null | ''): T | null {
|
||||||
|
return value === undefined || value === null || value === '' ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJsonArray(value: string[] | string | null | undefined): string[] | null {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length > 0 ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string' && value.trim() !== '') {
|
||||||
|
if (value.trim().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return Array.isArray(parsed) ? parsed : [value];
|
||||||
|
} catch {
|
||||||
|
return [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitValues = value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return splitValues.length > 0 ? splitValues : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
|
||||||
|
if (!value) return null;
|
||||||
|
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(value: string | number | null | undefined): number | null {
|
||||||
|
if (value === null || value === undefined || value === '') return null;
|
||||||
|
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||||||
|
return isNaN(num) ? null : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSqlOnError(statement: { sql: string; params: unknown[] } | null): void {
|
||||||
|
if (!statement) return;
|
||||||
|
console.error(` SQL: ${statement.sql}`);
|
||||||
|
console.error(` Params: ${JSON.stringify(statement.params)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCharacterData(item: CharacterRecord) {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
gender: toNullable(item.gender),
|
||||||
|
age: toNullable(item.age),
|
||||||
|
affiliations: toJsonArray(item.affiliations),
|
||||||
|
devilFruitId: toNullable(item.devilFruitId),
|
||||||
|
hakiObservation: !!item.hakiObservation,
|
||||||
|
hakiArmament: !!item.hakiArmament,
|
||||||
|
hakiConqueror: !!item.hakiConqueror,
|
||||||
|
bounty: item.bounty ?? 0,
|
||||||
|
height: toNumber(item.height as any),
|
||||||
|
origin: toNullable(item.origin),
|
||||||
|
firstAppearance: item.firstAppearance ?? 0,
|
||||||
|
pictureUrl: toNullable(item.pictureUrl),
|
||||||
|
epithets: toJsonArray(item.epithets),
|
||||||
|
status: toNullable(item.status),
|
||||||
|
arcId: toNullable(item.arcId),
|
||||||
|
url: toNullable(item.url)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChanged(jsonData: any, dbData: any): boolean {
|
||||||
|
if (!dbData) return true;
|
||||||
|
|
||||||
|
// Print any differences for debugging
|
||||||
|
for (const key in jsonData) {
|
||||||
|
const jsonValue = jsonData[key];
|
||||||
|
const dbValue = dbData[key];
|
||||||
|
const jsonString = typeof jsonValue === 'object' ? JSON.stringify(jsonValue) : String(jsonValue);
|
||||||
|
const dbString = typeof dbValue === 'object' ? JSON.stringify(dbValue) : String(dbValue);
|
||||||
|
if (jsonString !== dbString) {
|
||||||
|
console.log(`\nField "${key}" changed for character ID ${jsonData.id}:`);
|
||||||
|
console.log(` JSON: ${jsonString}`);
|
||||||
|
console.log(` DB: ${dbString}`);
|
||||||
|
} }
|
||||||
|
|
||||||
|
// Compare each field
|
||||||
|
return (
|
||||||
|
jsonData.name != dbData.name ||
|
||||||
|
jsonData.gender != dbData.gender ||
|
||||||
|
jsonData.age != dbData.age ||
|
||||||
|
JSON.stringify(jsonData.affiliations) != JSON.stringify(dbData.affiliations) ||
|
||||||
|
jsonData.devilFruitId != dbData.devilFruitId ||
|
||||||
|
jsonData.hakiObservation != dbData.hakiObservation ||
|
||||||
|
jsonData.hakiArmament != dbData.hakiArmament ||
|
||||||
|
jsonData.hakiConqueror != dbData.hakiConqueror ||
|
||||||
|
jsonData.bounty != dbData.bounty ||
|
||||||
|
jsonData.height != dbData.height ||
|
||||||
|
jsonData.origin != dbData.origin ||
|
||||||
|
jsonData.firstAppearance != dbData.firstAppearance ||
|
||||||
|
jsonData.pictureUrl != dbData.pictureUrl ||
|
||||||
|
JSON.stringify(jsonData.epithets) != JSON.stringify(dbData.epithets) ||
|
||||||
|
jsonData.status != dbData.status ||
|
||||||
|
jsonData.arcId != dbData.arcId ||
|
||||||
|
jsonData.url != dbData.url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isCharacterTableEmpty(): Promise<boolean> {
|
||||||
|
const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character);
|
||||||
|
return result[0]?.count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importFromJson(): Promise<void> {
|
||||||
|
let totalSuccess = 0;
|
||||||
|
let totalErrors = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arcs = readJsonFile<ArcRecord>('./scraped-data/arcs.json');
|
||||||
|
if (arcs) {
|
||||||
|
console.log('\n=== Importing Arcs ===\n');
|
||||||
|
console.log(`Found ${arcs.length} arcs\n`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < arcs.length; i++) {
|
||||||
|
const item = arcs[i];
|
||||||
|
let lastSql: { sql: string; params: unknown[] } | null = null;
|
||||||
|
try {
|
||||||
|
const query = db
|
||||||
|
.insert(arc)
|
||||||
|
.values({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
startChapter: item.startChapter,
|
||||||
|
endChapter: toNullable(item.endChapter),
|
||||||
|
url: toNullable(item.url)
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: arc.id,
|
||||||
|
set: {
|
||||||
|
name: item.name,
|
||||||
|
startChapter: item.startChapter,
|
||||||
|
endChapter: toNullable(item.endChapter),
|
||||||
|
url: toNullable(item.url)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lastSql = query.toSQL();
|
||||||
|
await query;
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
process.stdout.write(`\rExecuted: ${successCount}/${arcs.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`\n✗ Error at arc ${i + 1}:`);
|
||||||
|
console.error(` ID: ${item.id ?? 'N/A'}`);
|
||||||
|
console.error(` Message: ${getErrorMessage(error)}`);
|
||||||
|
logSqlOnError(lastSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n\n✓ Arcs imported!`);
|
||||||
|
console.log(` Success: ${successCount}`);
|
||||||
|
console.log(` Errors: ${errorCount}`);
|
||||||
|
|
||||||
|
totalSuccess += successCount;
|
||||||
|
totalErrors += errorCount;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ No arcs.json found, skipping...\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fruits = readJsonFile<DevilFruitRecord>('./scraped-data/devil-fruits.json');
|
||||||
|
if (fruits) {
|
||||||
|
console.log('\n=== Importing Devil Fruits ===\n');
|
||||||
|
console.log(`Found ${fruits.length} devil fruits\n`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < fruits.length; i++) {
|
||||||
|
const item = fruits[i];
|
||||||
|
let lastSql: { sql: string; params: unknown[] } | null = null;
|
||||||
|
try {
|
||||||
|
const query = db
|
||||||
|
.insert(devilFruit)
|
||||||
|
.values({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: toDevilFruitType(item.type),
|
||||||
|
url: toNullable(item.url)
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: devilFruit.id,
|
||||||
|
set: {
|
||||||
|
name: item.name,
|
||||||
|
type: toDevilFruitType(item.type),
|
||||||
|
url: toNullable(item.url)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lastSql = query.toSQL();
|
||||||
|
await query;
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
process.stdout.write(`\rExecuted: ${successCount}/${fruits.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`\n✗ Error at devil fruit ${i + 1}:`);
|
||||||
|
console.error(` ID: ${item.id ?? 'N/A'}`);
|
||||||
|
console.error(` Message: ${getErrorMessage(error)}`);
|
||||||
|
logSqlOnError(lastSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n\n✓ Devil Fruits imported!`);
|
||||||
|
console.log(` Success: ${successCount}`);
|
||||||
|
console.log(` Errors: ${errorCount}`);
|
||||||
|
|
||||||
|
totalSuccess += successCount;
|
||||||
|
totalErrors += errorCount;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ No devil-fruits.json found, skipping...\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters = readJsonFile<CharacterRecord>('./scraped-data/characters.json');
|
||||||
|
if (characters) {
|
||||||
|
console.log('\n=== Importing Characters ===\n');
|
||||||
|
console.log(`Found ${characters.length} characters\n`);
|
||||||
|
|
||||||
|
const isEmpty = await isCharacterTableEmpty();
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
// Populate empty character table
|
||||||
|
console.log('Characters table is empty, populating...\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < characters.length; i++) {
|
||||||
|
const item = characters[i];
|
||||||
|
let lastSql: { sql: string; params: unknown[] } | null = null;
|
||||||
|
try {
|
||||||
|
const data = transformCharacterData(item);
|
||||||
|
const query = db
|
||||||
|
.insert(character)
|
||||||
|
.values(data)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: character.id,
|
||||||
|
set: data
|
||||||
|
});
|
||||||
|
|
||||||
|
lastSql = query.toSQL();
|
||||||
|
await query;
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
process.stdout.write(`\rExecuted: ${successCount}/${characters.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`\n✗ Error at character ${i + 1}:`);
|
||||||
|
console.error(` ID: ${item.id ?? 'N/A'}`);
|
||||||
|
console.error(` Message: ${getErrorMessage(error)}`);
|
||||||
|
logSqlOnError(lastSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check for changes and update scrapeValidation table
|
||||||
|
console.log('Characters table not empty, checking for changes...\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < characters.length; i++) {
|
||||||
|
const item = characters[i];
|
||||||
|
let lastSql: { sql: string; params: unknown[] } | null = null;
|
||||||
|
try {
|
||||||
|
const selectQuery = db
|
||||||
|
.select()
|
||||||
|
.from(character)
|
||||||
|
.where(eq(character.id, item.id));
|
||||||
|
|
||||||
|
lastSql = selectQuery.toSQL();
|
||||||
|
const [dbCharacter] = await selectQuery;
|
||||||
|
|
||||||
|
const jsonData = transformCharacterData(item);
|
||||||
|
const changed = hasChanged(jsonData, dbCharacter);
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
// Update scrapeValidation table with changes
|
||||||
|
const upsertQuery = db
|
||||||
|
.insert(characterScrapeValidation)
|
||||||
|
.values(jsonData)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: characterScrapeValidation.id,
|
||||||
|
set: jsonData
|
||||||
|
});
|
||||||
|
|
||||||
|
lastSql = upsertQuery.toSQL();
|
||||||
|
await upsertQuery;
|
||||||
|
} else {
|
||||||
|
// No changes, delete from scrapeValidation if it exists
|
||||||
|
const deleteQuery = db
|
||||||
|
.delete(characterScrapeValidation)
|
||||||
|
.where(eq(characterScrapeValidation.id, item.id));
|
||||||
|
|
||||||
|
lastSql = deleteQuery.toSQL();
|
||||||
|
await deleteQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`\n✗ Error at character ${i + 1}:`);
|
||||||
|
console.error(` ID: ${item.id ?? 'N/A'}`);
|
||||||
|
console.error(` Message: ${getErrorMessage(error)}`);
|
||||||
|
logSqlOnError(lastSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n\n✓ Characters imported!`);
|
||||||
|
console.log(` Success: ${successCount}`);
|
||||||
|
console.log(` Errors: ${errorCount}`);
|
||||||
|
|
||||||
|
totalSuccess += successCount;
|
||||||
|
totalErrors += errorCount;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ No characters.json found, skipping...\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Total Import Summary ===`);
|
||||||
|
console.log(` Total Success: ${totalSuccess}`);
|
||||||
|
console.log(` Total Errors: ${totalErrors}\n`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Import failed:', getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importFromJson().catch((error) => {
|
||||||
|
console.error(getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { createClient } from '@libsql/client';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
|
|
||||||
|
|
||||||
const client = createClient({
|
|
||||||
url: DATABASE_URL
|
|
||||||
});
|
|
||||||
|
|
||||||
async function importSQL() {
|
|
||||||
try {
|
|
||||||
let totalSuccess = 0;
|
|
||||||
let totalErrors = 0;
|
|
||||||
|
|
||||||
// Step 1: Import Devil Fruits
|
|
||||||
if (fs.existsSync('./scraped-data/devil-fruits.sql')) {
|
|
||||||
console.log('\n=== Importing Devil Fruits ===\n');
|
|
||||||
const devilFruitsSql = fs.readFileSync('./scraped-data/devil-fruits.sql', 'utf-8');
|
|
||||||
const dfStatements = devilFruitsSql.split(';\n\n').filter(s => s.trim());
|
|
||||||
|
|
||||||
console.log(`Found ${dfStatements.length} devil fruit statements\n`);
|
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < dfStatements.length; i++) {
|
|
||||||
const statement = dfStatements[i];
|
|
||||||
if (statement.trim()) {
|
|
||||||
try {
|
|
||||||
await client.execute(statement.trim() + ';');
|
|
||||||
successCount++;
|
|
||||||
process.stdout.write(`\rExecuted: ${successCount}/${dfStatements.length}`);
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
const valuesMatch = statement.match(/VALUES\s*\(([^)]+)\)/);
|
|
||||||
const values = valuesMatch ? valuesMatch[1] : 'N/A';
|
|
||||||
console.error(`\n✗ Error at statement ${i + 1}:`);
|
|
||||||
console.error(` Values: ${values}`);
|
|
||||||
console.error(` Message: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n\n✓ Devil Fruits imported!`);
|
|
||||||
console.log(` Success: ${successCount}`);
|
|
||||||
console.log(` Errors: ${errorCount}`);
|
|
||||||
|
|
||||||
totalSuccess += successCount;
|
|
||||||
totalErrors += errorCount;
|
|
||||||
} else {
|
|
||||||
console.log('\n⚠️ No devil-fruits.sql found, skipping...\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Import Characters
|
|
||||||
if (fs.existsSync('./scraped-data/characters.sql')) {
|
|
||||||
console.log('\n=== Importing Characters ===\n');
|
|
||||||
const charactersSql = fs.readFileSync('./scraped-data/characters.sql', 'utf-8');
|
|
||||||
const charStatements = charactersSql.split(';\n\n').filter(s => s.trim());
|
|
||||||
|
|
||||||
console.log(`Found ${charStatements.length} character statements\n`);
|
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < charStatements.length; i++) {
|
|
||||||
const statement = charStatements[i];
|
|
||||||
if (statement.trim()) {
|
|
||||||
try {
|
|
||||||
await client.execute(statement.trim() + ';');
|
|
||||||
successCount++;
|
|
||||||
process.stdout.write(`\rExecuted: ${successCount}/${charStatements.length}`);
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
const valuesMatch = statement.match(/VALUES\s*\(([^)]+)\)/);
|
|
||||||
const values = valuesMatch ? valuesMatch[1] : 'N/A';
|
|
||||||
console.error(`\n✗ Error at statement ${i + 1}:`);
|
|
||||||
console.error(` Values: ${values}`);
|
|
||||||
console.error(` Message: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n\n✓ Characters imported!`);
|
|
||||||
console.log(` Success: ${successCount}`);
|
|
||||||
console.log(` Errors: ${errorCount}`);
|
|
||||||
|
|
||||||
totalSuccess += successCount;
|
|
||||||
totalErrors += errorCount;
|
|
||||||
} else {
|
|
||||||
console.log('\n⚠️ No characters.sql found, skipping...\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n=== Total Import Summary ===`);
|
|
||||||
console.log(` Total Success: ${totalSuccess}`);
|
|
||||||
console.log(` Total Errors: ${totalErrors}\n`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('✗ Import failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
importSQL().catch(console.error);
|
|
||||||
64
scripts/init-column-config.ts
Normal file
64
scripts/init-column-config.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Initialize config table with column visibility settings for characterHistory table
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { config } from '../src/lib/server/db/schema';
|
||||||
|
import { like } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
|
||||||
|
|
||||||
|
const client = createClient({ url: DATABASE_URL });
|
||||||
|
const db = drizzle(client);
|
||||||
|
|
||||||
|
// Define the columns in characterHistory table
|
||||||
|
const columns = [
|
||||||
|
'gender',
|
||||||
|
'affiliations',
|
||||||
|
'haki',
|
||||||
|
'bounty',
|
||||||
|
'height',
|
||||||
|
'origin',
|
||||||
|
'devilFruitType',
|
||||||
|
'arc',
|
||||||
|
'status'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function initColumnConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('Initializing column visibility config...\n');
|
||||||
|
|
||||||
|
for (const column of columns) {
|
||||||
|
const key = `characterHistory.column.${column}.visible`;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
await db.insert(config)
|
||||||
|
.values({ key, value })
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
console.log(`✓ Added config key: ${key} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✓ Successfully initialized column visibility config');
|
||||||
|
|
||||||
|
// Display all config entries
|
||||||
|
const allConfig = await db.select()
|
||||||
|
.from(config)
|
||||||
|
.where(like(config.key, 'characterHistory.column.%.visible'));
|
||||||
|
|
||||||
|
console.log('\nCurrent column visibility config:');
|
||||||
|
allConfig.forEach(row => {
|
||||||
|
console.log(` ${row.key}: ${row.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing config:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initColumnConfig();
|
||||||
File diff suppressed because it is too large
Load Diff
84
scripts/set-daily-mode.ts
Normal file
84
scripts/set-daily-mode.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { character } from '../src/lib/server/db/schema';
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
|
||||||
|
|
||||||
|
const client = createClient({ url: DATABASE_URL });
|
||||||
|
const db = drizzle(client);
|
||||||
|
|
||||||
|
function readJsonFile(path: string): string[] | null {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(path, 'utf-8');
|
||||||
|
return JSON.parse(content) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDailyCharacters(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const dailyCharacterIds = readJsonFile('./scripts/daily-characters.json');
|
||||||
|
|
||||||
|
if (!dailyCharacterIds || dailyCharacterIds.length === 0) {
|
||||||
|
console.error('❌ No daily characters found in daily-characters.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Setting Daily Mode Characters ===\n`);
|
||||||
|
console.log(`Found ${dailyCharacterIds.length} characters to set as daily\n`);
|
||||||
|
|
||||||
|
// Step 1: Disable isInDailyMode for all characters
|
||||||
|
console.log('Step 1: Disabling isInDailyMode for all characters...');
|
||||||
|
await db.update(character).set({ isInDailyMode: false });
|
||||||
|
console.log('✓ All characters disabled from daily mode\n');
|
||||||
|
|
||||||
|
// Step 2: Enable isInDailyMode for characters in the list
|
||||||
|
console.log('Step 2: Enabling isInDailyMode for daily characters...\n');
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < dailyCharacterIds.length; i++) {
|
||||||
|
const charId = dailyCharacterIds[i];
|
||||||
|
try {
|
||||||
|
const result = await db
|
||||||
|
.update(character)
|
||||||
|
.set({ isInDailyMode: true })
|
||||||
|
.where(eq(character.id, charId));
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
process.stdout.write(`\rUpdated: ${successCount}/${dailyCharacterIds.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`\n✗ Error updating character ${i + 1}:`);
|
||||||
|
console.error(` ID: ${charId}`);
|
||||||
|
console.error(` Message: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n\n✓ Daily characters updated!`);
|
||||||
|
console.log(` Success: ${successCount}`);
|
||||||
|
console.log(` Errors: ${errorCount}`);
|
||||||
|
|
||||||
|
if (errorCount === 0) {
|
||||||
|
console.log(`\n✅ Successfully set ${successCount} characters as daily mode characters\n`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Operation failed:', getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDailyCharacters().catch((error) => {
|
||||||
|
console.error(getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,8 +1,36 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { building } from '$app/environment';
|
import { building } from '$app/environment';
|
||||||
import { auth } from '$lib/server/auth';
|
import { auth } from '$lib/server/auth';
|
||||||
|
import { getDailyModeCharacters, getOrCreateTodayCharacter } from '$lib/server/daily-character';
|
||||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __dailyCharacterSchedulerStarted: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDailyCharacterSchedulerJob() {
|
||||||
|
try {
|
||||||
|
const characters = await getDailyModeCharacters();
|
||||||
|
if (characters.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getOrCreateTodayCharacter(characters);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Daily character scheduler failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!building && !globalThis.__dailyCharacterSchedulerStarted) {
|
||||||
|
globalThis.__dailyCharacterSchedulerStarted = true;
|
||||||
|
|
||||||
|
void runDailyCharacterSchedulerJob();
|
||||||
|
setInterval(() => {
|
||||||
|
void runDailyCharacterSchedulerJob();
|
||||||
|
}, 60_000);
|
||||||
|
}
|
||||||
|
|
||||||
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||||
|
|
||||||
|
|||||||
152
src/lib/server/daily-character.ts
Normal file
152
src/lib/server/daily-character.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { arc, character, characterHistory, devilFruit } from '$lib/server/db/schema';
|
||||||
|
import { desc, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
const characterWithRelationsSelect = {
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
gender: character.gender,
|
||||||
|
age: character.age,
|
||||||
|
affiliations: character.affiliations,
|
||||||
|
devilFruitId: character.devilFruitId,
|
||||||
|
devilFruitName: devilFruit.name,
|
||||||
|
devilFruitType: devilFruit.type,
|
||||||
|
hakiObservation: character.hakiObservation,
|
||||||
|
hakiArmament: character.hakiArmament,
|
||||||
|
hakiConqueror: character.hakiConqueror,
|
||||||
|
bounty: character.bounty,
|
||||||
|
height: character.height,
|
||||||
|
origin: character.origin,
|
||||||
|
firstAppearance: character.firstAppearance,
|
||||||
|
pictureUrl: character.pictureUrl,
|
||||||
|
epithets: character.epithets,
|
||||||
|
status: character.status,
|
||||||
|
url: character.url,
|
||||||
|
arcId: character.arcId,
|
||||||
|
arcName: arc.name
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CharacterWithRelations = typeof character.$inferSelect & {
|
||||||
|
devilFruitName: string | null;
|
||||||
|
devilFruitType: string | null;
|
||||||
|
arcName: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDateKey(date: Date): string {
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDay(date: Date = new Date()): Date {
|
||||||
|
const normalized = new Date(date);
|
||||||
|
normalized.setHours(0, 0, 0, 0);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): CharacterWithRelations {
|
||||||
|
const dateStr = getDateKey(date);
|
||||||
|
const seed = dateStr.split('-').reduce((acc, value) => acc + parseInt(value), 0);
|
||||||
|
const index = seed % characters.length;
|
||||||
|
return characters[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
|
||||||
|
return db
|
||||||
|
.select(characterWithRelationsSelect)
|
||||||
|
.from(character)
|
||||||
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
|
.where(eq(character.isInDailyMode, true))
|
||||||
|
.all() as Promise<CharacterWithRelations[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
|
||||||
|
const [found] = await db
|
||||||
|
.select(characterWithRelationsSelect)
|
||||||
|
.from(character)
|
||||||
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
|
.where(eq(character.id, characterId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return (found ?? null) as CharacterWithRelations | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateTodayCharacter(
|
||||||
|
characters: CharacterWithRelations[],
|
||||||
|
date: Date = new Date()
|
||||||
|
): Promise<CharacterWithRelations | null> {
|
||||||
|
if (characters.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = normalizeDay(date);
|
||||||
|
const todayDate = getDateKey(today);
|
||||||
|
|
||||||
|
const [existingEntry] = await db
|
||||||
|
.select()
|
||||||
|
.from(characterHistory)
|
||||||
|
.where(eq(characterHistory.date, todayDate))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingEntry?.characterId) {
|
||||||
|
return (
|
||||||
|
characters.find((currentCharacter) => currentCharacter.id === existingEntry.characterId) ??
|
||||||
|
(await getCharacterById(existingEntry.characterId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentHistory = await db
|
||||||
|
.select({ characterId: characterHistory.characterId })
|
||||||
|
.from(characterHistory)
|
||||||
|
.orderBy(desc(characterHistory.date))
|
||||||
|
.limit(100);
|
||||||
|
|
||||||
|
const excludedIds = new Set(recentHistory.map((entry) => entry.characterId));
|
||||||
|
const availableCharacters = characters.filter((currentCharacter) => !excludedIds.has(currentCharacter.id));
|
||||||
|
|
||||||
|
const dailyCharacter = pickDailyCharacter(
|
||||||
|
availableCharacters.length > 0 ? availableCharacters : characters,
|
||||||
|
today
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.insert(characterHistory).values({
|
||||||
|
characterId: dailyCharacter.id,
|
||||||
|
date: todayDate,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to record daily character:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dailyCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getYesterdayCharacter(
|
||||||
|
date: Date = new Date(),
|
||||||
|
characters?: CharacterWithRelations[]
|
||||||
|
): Promise<CharacterWithRelations | null> {
|
||||||
|
const baseDate = normalizeDay(date);
|
||||||
|
baseDate.setDate(baseDate.getDate() - 1);
|
||||||
|
const yesterdayDate = getDateKey(baseDate);
|
||||||
|
|
||||||
|
const [yesterdayEntry] = await db
|
||||||
|
.select()
|
||||||
|
.from(characterHistory)
|
||||||
|
.where(eq(characterHistory.date, yesterdayDate))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!yesterdayEntry?.characterId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characters) {
|
||||||
|
return (
|
||||||
|
characters.find((currentCharacter) => currentCharacter.id === yesterdayEntry.characterId) ??
|
||||||
|
(await getCharacterById(yesterdayEntry.characterId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCharacterById(yesterdayEntry.characterId);
|
||||||
|
}
|
||||||
@@ -1,16 +1,29 @@
|
|||||||
import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core';
|
import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
// Define haki types
|
|
||||||
export type HakiType = 'Observation' | 'Armament' | 'Conqueror';
|
|
||||||
|
|
||||||
// Define devil fruit types
|
// Define devil fruit types
|
||||||
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown';
|
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown';
|
||||||
|
|
||||||
|
// Define the site config table schema
|
||||||
|
export const config = sqliteTable('config', {
|
||||||
|
key: text('key').primaryKey(),
|
||||||
|
value: text('value')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the arc table schema
|
||||||
|
export const arc = sqliteTable('arc', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
startChapter: integer('startChapter').notNull(),
|
||||||
|
endChapter: integer('endChapter'),
|
||||||
|
url: text('url')
|
||||||
|
});
|
||||||
|
|
||||||
// Define the devil fruit table schema
|
// Define the devil fruit table schema
|
||||||
export const devilFruit = sqliteTable('devilFruit', {
|
export const devilFruit = sqliteTable('devilFruit', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
name: text('name').notNull().unique(),
|
name: text('name').notNull().unique(),
|
||||||
type: text('type').$type<DevilFruitType>()
|
type: text('type').$type<DevilFruitType>(),
|
||||||
|
url: text('url')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the character table schema
|
// Define the character table schema
|
||||||
@@ -19,15 +32,66 @@ export const character = sqliteTable('character', {
|
|||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
gender: text('gender'),
|
gender: text('gender'),
|
||||||
age: integer('age'),
|
age: integer('age'),
|
||||||
affiliations: text('affiliations'),
|
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
||||||
devilFruit: text('devilFruit').references(() => devilFruit.id),
|
devilFruitId: text('devilFruitId').references(() => devilFruit.id),
|
||||||
haki: text('haki', { mode: 'json' }).$type<HakiType[]>(),
|
hakiObservation: integer('hakiObservation', { mode: 'boolean' }).default(false),
|
||||||
bounty: integer('bounty'),
|
hakiArmament: integer('hakiArmament', { mode: 'boolean' }).default(false),
|
||||||
// height in meters as a float (e.g. 1.75)
|
hakiConqueror: integer('hakiConqueror', { mode: 'boolean' }).default(false),
|
||||||
|
bounty: integer('bounty').default(0),
|
||||||
height: real('height'),
|
height: real('height'),
|
||||||
origin: text('origin'),
|
origin: text('origin'),
|
||||||
firstAppearance: text('firstAppearance'),
|
firstAppearance: integer('firstAppearance').notNull(),
|
||||||
pictureUrl: text('pictureUrl')
|
pictureUrl: text('pictureUrl'),
|
||||||
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
|
status: text('status'),
|
||||||
|
arcId: text('arcId').references(() => arc.id),
|
||||||
|
url: text('url'),
|
||||||
|
isInDailyMode: integer('isInDailyMode', { mode: 'boolean' }).default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the character override table schema
|
||||||
|
export const characterOverride = sqliteTable('characterOverride', {
|
||||||
|
characterId: text('characterId').primaryKey().references(() => character.id),
|
||||||
|
name: text('name'),
|
||||||
|
gender: text('gender'),
|
||||||
|
age: integer('age'),
|
||||||
|
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
||||||
|
devilFruitId: text('devilFruitId').references(() => devilFruit.id),
|
||||||
|
hakiObservation: integer('hakiObservation', { mode: 'boolean' }),
|
||||||
|
hakiArmament: integer('hakiArmament', { mode: 'boolean' }),
|
||||||
|
hakiConqueror: integer('hakiConqueror', { mode: 'boolean' }),
|
||||||
|
bounty: integer('bounty'),
|
||||||
|
height: real('height'),
|
||||||
|
origin: text('origin'),
|
||||||
|
firstAppearance: integer('firstAppearance').notNull(),
|
||||||
|
pictureUrl: text('pictureUrl'),
|
||||||
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
|
status: text('status'),
|
||||||
|
arcId: text('arcId').references(() => arc.id),
|
||||||
|
url: text('url'),
|
||||||
|
notes: text('notes')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the character scrape validation table schema
|
||||||
|
export const characterScrapeValidation = sqliteTable('characterScrapeValidation', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
gender: text('gender'),
|
||||||
|
age: integer('age'),
|
||||||
|
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
||||||
|
devilFruitId: text('devilFruitId').references(() => devilFruit.id),
|
||||||
|
hakiObservation: integer('hakiObservation', { mode: 'boolean' }).default(false),
|
||||||
|
hakiArmament: integer('hakiArmament', { mode: 'boolean' }).default(false),
|
||||||
|
hakiConqueror: integer('hakiConqueror', { mode: 'boolean' }).default(false),
|
||||||
|
bounty: integer('bounty'),
|
||||||
|
height: real('height'),
|
||||||
|
origin: text('origin'),
|
||||||
|
firstAppearance: integer('firstAppearance').notNull(),
|
||||||
|
pictureUrl: text('pictureUrl'),
|
||||||
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
|
status: text('status'),
|
||||||
|
arcId: text('arcId').references(() => arc.id),
|
||||||
|
url: text('url')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the caracter history table schema
|
// Define the caracter history table schema
|
||||||
@@ -36,9 +100,11 @@ export const characterHistory = sqliteTable('characterHistory', {
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
characterId: text('characterId').references(() => character.id),
|
characterId: text('characterId').references(() => character.id),
|
||||||
date: integer('date'),
|
date: text('date'),
|
||||||
|
won: integer('won').notNull().default(0),
|
||||||
createdAt: integer('createdAt').notNull().$default(() => Date.now()),
|
createdAt: integer('createdAt').notNull().$default(() => Date.now()),
|
||||||
updatedAt: integer('updatedAt').notNull().$default(() => Date.now()),
|
updatedAt: integer('updatedAt').notNull().$default(() => Date.now()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export * from './auth.schema';
|
export * from './auth.schema';
|
||||||
|
|||||||
9
src/routes/+page.server.ts
Normal file
9
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { getYesterdayCharacter } from '$lib/server/daily-character';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
const yesterdayCharacter = await getYesterdayCharacter();
|
||||||
|
|
||||||
|
return {
|
||||||
|
yesterdayCharacter: yesterdayCharacter || null
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
$: yesterdayCharacter = data.yesterdayCharacter;
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>OnePieceDle</title>
|
<title>OnePieceDle</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
|
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
|
||||||
style="background-image: url('/one-piece-bg.jpg');"
|
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
||||||
<div class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"></div>
|
<div class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"></div>
|
||||||
|
|
||||||
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col items-center justify-between px-6 py-24 sm:py-28">
|
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col items-center justify-between px-6 py-10">
|
||||||
<div class="flex w-full flex-1 flex-col items-center justify-between gap-12">
|
<div class="flex w-full flex-1 flex-col items-center justify-between gap-12">
|
||||||
<div class="rounded-full border border-amber-200/30 bg-amber-100/10 px-5 py-2 text-xs font-semibold uppercase tracking-[0.35em] text-amber-100">
|
|
||||||
Jeu de devinettes Grand Line
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-4xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-6xl">
|
<h1 class="text-4xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-6xl">
|
||||||
OnePieceDle
|
OnePieceDle
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid w-full gap-4 sm:grid-cols-2">
|
<div class="grid w-full gap-4 sm:grid-cols-2">
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Voyage du jour</h2>
|
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Personnage du jour</h2>
|
||||||
<p class="mt-3 text-lg font-semibold text-white">Nouveau mystere toutes les 24 heures</p>
|
<p class="mt-3 text-lg font-semibold text-white">Nouveau mystere toutes les 24 heures</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Compare tes essais, debloque des indices et garde ta serie.</p>
|
<p class="mt-2 text-sm text-slate-200">Compare tes essais, debloque des indices et garde ta serie.</p>
|
||||||
<a
|
<a
|
||||||
@@ -39,24 +41,56 @@
|
|||||||
<p class="mt-3 text-lg font-semibold text-white">Entraine-toi avec des pirates legendaires</p>
|
<p class="mt-3 text-lg font-semibold text-white">Entraine-toi avec des pirates legendaires</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Choisis une epoque, regle la difficulte et vogue a ton rythme.</p>
|
<p class="mt-2 text-sm text-slate-200">Choisis une epoque, regle la difficulte et vogue a ton rythme.</p>
|
||||||
<button class="mt-5 w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50">
|
<button class="mt-5 w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50">
|
||||||
Choisir un voyage
|
En construction
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
|
<div class="w-full rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
|
||||||
|
{#if yesterdayCharacter}
|
||||||
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
|
{#if yesterdayCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={yesterdayCharacter.pictureUrl}
|
||||||
|
alt={yesterdayCharacter.name}
|
||||||
|
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
|
Photo
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
|
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
|
||||||
|
{#if yesterdayCharacter.epithets}
|
||||||
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{typeof yesterdayCharacter.epithets === 'string'
|
||||||
|
? JSON.parse(yesterdayCharacter.epithets).join(', ')
|
||||||
|
: (yesterdayCharacter.epithets as string[]).join(', ')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
|
||||||
|
>
|
||||||
|
Voir la page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
Photo
|
Photo
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage de la veille</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-white">Placeholder</p>
|
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
|
||||||
<p class="mt-1 text-sm text-slate-200">Le revele sera visible apres la partie du jour.</p>
|
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto">
|
|
||||||
Voir l'archive
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
38
src/routes/daily/+page.server.ts
Normal file
38
src/routes/daily/+page.server.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { config } from '$lib/server/db/schema';
|
||||||
|
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter } from '$lib/server/daily-character';
|
||||||
|
import { like } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
const characters = await getDailyModeCharacters();
|
||||||
|
const dailyCharacter = await getOrCreateTodayCharacter(characters);
|
||||||
|
|
||||||
|
if (!dailyCharacter) {
|
||||||
|
throw error(404, 'No daily character available. Please check if characters are configured in daily mode.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const yesterdayCharacter = await getYesterdayCharacter(new Date(), characters);
|
||||||
|
|
||||||
|
// Load column visibility config
|
||||||
|
const columnConfig = await db
|
||||||
|
.select()
|
||||||
|
.from(config)
|
||||||
|
.where(like(config.key, 'characterHistory.column.%.visible'));
|
||||||
|
|
||||||
|
// Convert to object for easier access
|
||||||
|
const columnVisibility: Record<string, boolean> = {};
|
||||||
|
columnConfig.forEach(row => {
|
||||||
|
const match = row.key.match(/characterHistory\.column\.(.+)\.visible/);
|
||||||
|
if (match) {
|
||||||
|
columnVisibility[match[1]] = row.value === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
characters,
|
||||||
|
dailyCharacter,
|
||||||
|
yesterdayCharacter,
|
||||||
|
columnVisibility
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,87 +1,772 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let searchInput = '';
|
||||||
|
let selectedCharacters: any[] = [];
|
||||||
|
let highlightedIndex = 0;
|
||||||
|
let isLoaded = false;
|
||||||
|
let isGeckoMoriaWin = false;
|
||||||
|
let dropdownContainer: HTMLDivElement;
|
||||||
|
let showHintOrigin = false;
|
||||||
|
let showHintFruit = false;
|
||||||
|
let showHintAffiliation = false;
|
||||||
|
|
||||||
|
let wasOriginAvailable = false;
|
||||||
|
let wasFruitAvailable = false;
|
||||||
|
let wasAffiliationAvailable = false;
|
||||||
|
let showOriginUnlock = false;
|
||||||
|
let showFruitUnlock = false;
|
||||||
|
let showAffiliationUnlock = false;
|
||||||
|
|
||||||
|
// Load from localStorage on mount
|
||||||
|
onMount(() => {
|
||||||
|
const stored = localStorage.getItem('dailyCharacterHistory');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const storedIds = JSON.parse(stored);
|
||||||
|
// Reconstruct character objects from IDs
|
||||||
|
if (Array.isArray(storedIds)) {
|
||||||
|
selectedCharacters = storedIds
|
||||||
|
.map((id: string) => data.characters.find((c: any) => c.id === id))
|
||||||
|
.filter((c: any) => c !== undefined);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse stored history', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to localStorage whenever selectedCharacters changes (only store IDs)
|
||||||
|
$: if (isLoaded && selectedCharacters) {
|
||||||
|
const ids = selectedCharacters.map(char => char.id);
|
||||||
|
localStorage.setItem('dailyCharacterHistory', JSON.stringify(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
$: characters = data.characters || [];
|
||||||
|
$: dailyCharacter = data.dailyCharacter;
|
||||||
|
$: yesterdayCharacter = data.yesterdayCharacter;
|
||||||
|
$: columnVisibility = data.columnVisibility || {};
|
||||||
|
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
|
||||||
|
|
||||||
|
// Hint availability - indices are available after a certain number of guesses
|
||||||
|
$: isOriginAvailable = selectedCharacters.length >= 5; // Always available
|
||||||
|
$: isFruitAvailable = selectedCharacters.length >= 10; // Available after 5 guesses
|
||||||
|
$: isAffiliationAvailable = selectedCharacters.length >= 15; // Available after 10 guesses
|
||||||
|
|
||||||
|
// Track hint unlocks
|
||||||
|
$: if (isLoaded) {
|
||||||
|
if (isOriginAvailable && !wasOriginAvailable) {
|
||||||
|
showOriginUnlock = true;
|
||||||
|
setTimeout(() => showOriginUnlock = false, 600);
|
||||||
|
}
|
||||||
|
wasOriginAvailable = isOriginAvailable;
|
||||||
|
|
||||||
|
if (isFruitAvailable && !wasFruitAvailable) {
|
||||||
|
showFruitUnlock = true;
|
||||||
|
setTimeout(() => showFruitUnlock = false, 600);
|
||||||
|
}
|
||||||
|
wasFruitAvailable = isFruitAvailable;
|
||||||
|
|
||||||
|
if (isAffiliationAvailable && !wasAffiliationAvailable) {
|
||||||
|
showAffiliationUnlock = true;
|
||||||
|
setTimeout(() => showAffiliationUnlock = false, 600);
|
||||||
|
}
|
||||||
|
wasAffiliationAvailable = isAffiliationAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredCharacters = characters.filter(char => {
|
||||||
|
const searchTerm = searchInput.toLowerCase();
|
||||||
|
const nameMatches = char.name.toLowerCase().includes(searchTerm);
|
||||||
|
|
||||||
|
let epithetsMatches = false;
|
||||||
|
if (char.epithets) {
|
||||||
|
try {
|
||||||
|
const parsedEpithets = typeof char.epithets === 'string'
|
||||||
|
? JSON.parse(char.epithets)
|
||||||
|
: char.epithets;
|
||||||
|
|
||||||
|
if (Array.isArray(parsedEpithets)) {
|
||||||
|
epithetsMatches = parsedEpithets.some((epithet: string) =>
|
||||||
|
epithet.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
} else if (typeof parsedEpithets === 'string') {
|
||||||
|
epithetsMatches = parsedEpithets.toLowerCase().includes(searchTerm);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
epithetsMatches = String(char.epithets).toLowerCase().includes(searchTerm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (nameMatches || epithetsMatches) &&
|
||||||
|
!selectedCharacters.some(selected => selected.id === char.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset highlighted index when filtered list changes
|
||||||
|
$: if (filteredCharacters) {
|
||||||
|
highlightedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll highlighted item into view
|
||||||
|
$: if (dropdownContainer && highlightedIndex >= 0) {
|
||||||
|
const highlightedButton = dropdownContainer.querySelector(
|
||||||
|
`button:nth-child(${highlightedIndex + 1})`
|
||||||
|
) as HTMLElement;
|
||||||
|
if (highlightedButton) {
|
||||||
|
highlightedButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCharacter(character: any) {
|
||||||
|
selectedCharacters = [character, ...selectedCharacters];
|
||||||
|
searchInput = '';
|
||||||
|
highlightedIndex = 0;
|
||||||
|
|
||||||
|
// Check if player won
|
||||||
|
if (character.id === dailyCharacter.id) {
|
||||||
|
// Send request to record win in database
|
||||||
|
fetch('/daily', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
characterId: dailyCharacter.id
|
||||||
|
})
|
||||||
|
}).catch(err => console.error('Failed to record win:', err));
|
||||||
|
|
||||||
|
// Check if it's gecko_moria for special animation
|
||||||
|
if (dailyCharacter.id === 'gecko_moria') {
|
||||||
|
isGeckoMoriaWin = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHistory() {
|
||||||
|
selectedCharacters = [];
|
||||||
|
localStorage.removeItem('dailyCharacterHistory');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (filteredCharacters.length === 0) return;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
if (filteredCharacters[highlightedIndex]) {
|
||||||
|
selectCharacter(filteredCharacters[highlightedIndex]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitGuess() {
|
||||||
|
if (filteredCharacters.length === 0) return;
|
||||||
|
const characterToSelect =
|
||||||
|
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
|
||||||
|
if (characterToSelect) {
|
||||||
|
selectCharacter(characterToSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBounty(bounty: number): string {
|
||||||
|
if (bounty >= 1_000_000_000) {
|
||||||
|
const billions = bounty / 1_000_000_000;
|
||||||
|
return `${billions}B`;
|
||||||
|
} else if (bounty >= 1_000_000) {
|
||||||
|
const millions = bounty / 1_000_000;
|
||||||
|
return `${millions}M`;
|
||||||
|
} else if (bounty >= 1_000) {
|
||||||
|
const thousands = bounty / 1_000;
|
||||||
|
return `${thousands}K`;
|
||||||
|
}
|
||||||
|
return bounty.toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>OnePieceDle - Mode du jour</title>
|
<title>OnePieceDle - Mode du jour</title>
|
||||||
|
<style>
|
||||||
|
@keyframes hint-unlock {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hint-unlocking {
|
||||||
|
animation: hint-unlock 0.6s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes shadow-pulse {
|
||||||
|
0% {
|
||||||
|
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow: 0 0 60px rgba(0, 0, 0, 0.9), 0 0 100px rgba(30, 30, 30, 1), inset 0 0 50px rgba(0, 0, 0, 0.7);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes moria-chaos {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
filter: invert(0%) hue-rotate(0deg) blur(0px);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: rotate(15deg) scale(1.02);
|
||||||
|
filter: invert(30%) hue-rotate(45deg) blur(2px);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: rotate(-10deg) scale(0.98);
|
||||||
|
filter: invert(60%) hue-rotate(90deg) blur(1px);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: rotate(25deg) scale(1.05);
|
||||||
|
filter: invert(100%) hue-rotate(180deg) blur(3px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: rotate(-20deg) scale(0.95);
|
||||||
|
filter: invert(80%) hue-rotate(270deg) blur(2px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(30deg) scale(1.08);
|
||||||
|
filter: invert(100%) hue-rotate(0deg) blur(4px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: rotate(-25deg) scale(0.92);
|
||||||
|
filter: invert(70%) hue-rotate(90deg) blur(2px);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: rotate(20deg) scale(1.03);
|
||||||
|
filter: invert(50%) hue-rotate(180deg) blur(3px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: rotate(-15deg) scale(1.01);
|
||||||
|
filter: invert(80%) hue-rotate(270deg) blur(1px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(1);
|
||||||
|
filter: invert(0%) hue-rotate(360deg) blur(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.gecko-moria-effect {
|
||||||
|
animation: shadow-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.moria-screen-chaos {
|
||||||
|
animation: moria-chaos 4s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
|
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}"
|
||||||
style="background-image: url('/one-piece-bg.jpg');"
|
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
||||||
<div class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"></div>
|
<div class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"></div>
|
||||||
|
|
||||||
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-16 sm:py-20">
|
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-16 sm:py-20">
|
||||||
<header class="flex flex-col items-start gap-6">
|
<nav class="absolute left-6 top-6 sm:left-8 sm:top-8">
|
||||||
<div class="rounded-full border border-amber-200/30 bg-amber-100/10 px-5 py-2 text-xs font-semibold uppercase tracking-[0.35em] text-amber-100">
|
<a
|
||||||
Mode du jour
|
href="/"
|
||||||
</div>
|
class="text-xl font-black uppercase tracking-[0.25em] text-amber-50 transition hover:text-amber-100"
|
||||||
|
>
|
||||||
|
OnePieceDle
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="flex flex-col items-start gap-6 w-full">
|
||||||
|
<div class="flex w-full items-center justify-between gap-4">
|
||||||
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
|
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
|
||||||
Mystere du jour
|
Personnage du jour
|
||||||
</h1>
|
</h1>
|
||||||
|
{#if hasWon}
|
||||||
|
<button
|
||||||
|
class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
|
||||||
|
onclick={resetHistory}
|
||||||
|
>
|
||||||
|
Recommencer
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
|
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
|
||||||
Devine le personnage. Chaque indice deblocque une nouvelle piste.
|
Devine le personnage. Chaque indice se débloque après un certain nombre de tentatives. Bonne chance !
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="mt-10 grid gap-6">
|
<section class="mt-10 grid gap-6">
|
||||||
|
{#if selectedCharacters.length > 0 && !hasWon}
|
||||||
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Indices du jour</h2>
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
<button
|
||||||
<div class="rounded-2xl border border-white/10 bg-slate-950/60 px-3 py-3">
|
type="button"
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-amber-100">Indice 1</p>
|
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isOriginAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showOriginUnlock ? 'hint-unlocking' : ''}"
|
||||||
<p class="mt-2 text-sm text-slate-200">Origine: ???</p>
|
disabled={!isOriginAvailable}
|
||||||
</div>
|
onclick={() => showHintOrigin = !showHintOrigin}
|
||||||
<div class="rounded-2xl border border-white/10 bg-slate-950/60 px-3 py-3">
|
>
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-amber-100">Indice 2</p>
|
<p class="text-sm font-medium text-amber-100">Origine</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Fruit du demon: ???</p>
|
{#if showHintOrigin}
|
||||||
</div>
|
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || 'Inconnue'}</p>
|
||||||
<div class="rounded-2xl border border-white/10 bg-slate-950/60 px-3 py-3">
|
{:else if Math.max(0, 5 - selectedCharacters.length) > 0}
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-amber-100">Indice 3</p>
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} essais avant déblocage</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Affiliation: ???</p>
|
{:else}
|
||||||
</div>
|
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isFruitAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showFruitUnlock ? 'hint-unlocking' : ''}"
|
||||||
|
disabled={!isFruitAvailable}
|
||||||
|
onclick={() => showHintFruit = !showHintFruit}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
|
||||||
|
{#if showHintFruit}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || 'Aucun'}</p>
|
||||||
|
{:else if Math.max(0, 10 - selectedCharacters.length) > 0}
|
||||||
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} essais avant déblocage</p>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isAffiliationAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showAffiliationUnlock ? 'hint-unlocking' : ''}"
|
||||||
|
disabled={!isAffiliationAvailable}
|
||||||
|
onclick={() => showHintAffiliation = !showHintAffiliation}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Affiliation</p>
|
||||||
|
{#if showHintAffiliation}
|
||||||
|
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
|
||||||
|
? (dailyCharacter.affiliations.includes('[') ? JSON.parse(dailyCharacter.affiliations) : dailyCharacter.affiliations.split(',').map((a: string) => a.trim()))
|
||||||
|
: dailyCharacter.affiliations}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">{Array.isArray(affiliations) ? affiliations[0] : affiliations || 'Inconnue'}</p>
|
||||||
|
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
|
||||||
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} essais avant déblocage</p>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
{#if hasWon}
|
||||||
|
{#if isGeckoMoriaWin}
|
||||||
|
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl mb-2">🌑</div>
|
||||||
|
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
|
||||||
|
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
{#if dailyCharacter.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={dailyCharacter.pictureUrl}
|
||||||
|
alt={dailyCharacter.name}
|
||||||
|
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl mb-2">🎉</div>
|
||||||
|
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
|
||||||
|
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
{#if dailyCharacter.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={dailyCharacter.pictureUrl}
|
||||||
|
alt={dailyCharacter.name}
|
||||||
|
class="w-20 h-20 mx-auto rounded-full border-2 border-emerald-400 shadow-lg object-cover hover:border-emerald-300 transition-colors cursor-pointer"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<p class="mt-2 text-lg font-bold text-white">{dailyCharacter.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur z-10">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
|
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
|
||||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
|
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<div class="relative w-full">
|
||||||
<input
|
<input
|
||||||
|
bind:value={searchInput}
|
||||||
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
|
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
|
||||||
placeholder="Nom du personnage"
|
placeholder="Nom du personnage"
|
||||||
type="text"
|
type="text"
|
||||||
|
onkeydown={handleKeydown}
|
||||||
/>
|
/>
|
||||||
<button class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200">
|
{#if searchInput.length > 0 && filteredCharacters.length > 0}
|
||||||
|
<div bind:this={dropdownContainer} class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-2xl border border-amber-200/30 bg-slate-900/90 backdrop-blur z-10">
|
||||||
|
{#each filteredCharacters as character, index (character.id)}
|
||||||
|
<button
|
||||||
|
class="w-full px-5 py-4 text-left text-base text-slate-100 transition border-b border-slate-800/50 last:border-b-0 flex items-center gap-4 {index === highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}"
|
||||||
|
type="button"
|
||||||
|
onmouseenter={() => highlightedIndex = index}
|
||||||
|
onclick={() => selectCharacter(character)}
|
||||||
|
>
|
||||||
|
{#if character.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={character.pictureUrl}
|
||||||
|
alt={character.name}
|
||||||
|
class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-12 h-12 rounded-full bg-slate-800 border border-amber-200/30 flex items-center justify-center">
|
||||||
|
<span class="text-xs text-slate-400">?</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="font-semibold text-amber-100">{character.name}</span>
|
||||||
|
{#if character.epithets}
|
||||||
|
{@const parsedEpithets = typeof character.epithets === 'string'
|
||||||
|
? JSON.parse(character.epithets)
|
||||||
|
: character.epithets}
|
||||||
|
{#if Array.isArray(parsedEpithets) && parsedEpithets.length > 0}
|
||||||
|
<span class="ml-2 text-xs text-slate-400">
|
||||||
|
• {parsedEpithets.join(', ')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={submitGuess}
|
||||||
|
disabled={filteredCharacters.length === 0}
|
||||||
|
class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
Valider
|
Valider
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4">
|
||||||
<div>
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Aucune tentative pour le moment.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50">
|
{#if selectedCharacters.length === 0}
|
||||||
Recommencer
|
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
|
||||||
</button>
|
{:else}
|
||||||
|
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
|
||||||
|
<div class="w-max min-w-max mx-auto">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
|
||||||
|
</div>
|
||||||
|
{#if columnVisibility.status !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.gender !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.affiliations !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.haki !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.bounty !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.height !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.origin !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.arc !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.devilFruitType !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rows -->
|
||||||
|
{#each selectedCharacters as character (character.id)}
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<!-- Personnage -->
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
|
||||||
|
{#if character.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block w-full h-full"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={character.pictureUrl}
|
||||||
|
alt={character.name}
|
||||||
|
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full h-full bg-slate-800 flex items-center justify-center p-2">
|
||||||
|
<span class="text-xl text-center font-semibold line-clamp-3">{character.name}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vivant / Mort -->
|
||||||
|
{#if columnVisibility.status !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-sm font-bold text-white text-center">
|
||||||
|
{character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Genre -->
|
||||||
|
{#if columnVisibility.gender !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-base font-bold text-white text-center">
|
||||||
|
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Affiliations -->
|
||||||
|
{#if columnVisibility.affiliations !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
||||||
|
try {
|
||||||
|
const charAff = typeof character.affiliations === 'string'
|
||||||
|
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
|
||||||
|
: character.affiliations;
|
||||||
|
const dailyAff = typeof dailyCharacter.affiliations === 'string'
|
||||||
|
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
||||||
|
: dailyCharacter.affiliations;
|
||||||
|
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
|
||||||
|
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
|
||||||
|
return charFirstAff && dailyFirstAff && charFirstAff === dailyFirstAff ? 'bg-emerald-600/90' : 'bg-red-900/60';
|
||||||
|
} catch (e) {
|
||||||
|
return 'bg-slate-950/60';
|
||||||
|
}
|
||||||
|
})()} p-2 flex items-center justify-center overflow-hidden">
|
||||||
|
{#if character.affiliations}
|
||||||
|
{@const parsedAffiliations = typeof character.affiliations === 'string'
|
||||||
|
? (character.affiliations.includes('[') ? JSON.parse(character.affiliations) : character.affiliations.split(',').map((a: string) => a.trim()))
|
||||||
|
: character.affiliations}
|
||||||
|
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
|
||||||
|
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="text-base font-bold text-slate-400 text-center">-</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Haki -->
|
||||||
|
{#if columnVisibility.haki !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
||||||
|
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
|
||||||
|
return 'bg-emerald-600/90';
|
||||||
|
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
|
||||||
|
(character.hakiArmament && dailyCharacter.hakiArmament) ||
|
||||||
|
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
|
||||||
|
return 'bg-yellow-600/80';
|
||||||
|
} else {
|
||||||
|
return 'bg-red-900/60';
|
||||||
|
}
|
||||||
|
})()} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-2xl font-bold text-white text-center">
|
||||||
|
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
|
||||||
|
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
|
||||||
|
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
|
||||||
|
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
|
||||||
|
<span class="text-5xl">✕</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Prime -->
|
||||||
|
{#if columnVisibility.bounty !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.bounty > dailyCharacter.bounty
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
{#if character.bounty != null}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Taille -->
|
||||||
|
{#if columnVisibility.height !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.height > dailyCharacter.height
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
{#if character.height}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Origine -->
|
||||||
|
{#if columnVisibility.origin !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Arc -->
|
||||||
|
{#if columnVisibility.arc !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Fruit -->
|
||||||
|
{#if columnVisibility.devilFruitType !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
{#if character.devilFruitType}
|
||||||
|
<p class="text-sm font-bold text-white text-center">{character.devilFruitType}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-5xl font-bold text-white text-center">✕</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
|
{#if yesterdayCharacter}
|
||||||
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
|
{#if yesterdayCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={yesterdayCharacter.pictureUrl}
|
||||||
|
alt={yesterdayCharacter.name}
|
||||||
|
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
|
Photo
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
|
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
|
||||||
|
{#if yesterdayCharacter.epithets}
|
||||||
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{typeof yesterdayCharacter.epithets === 'string'
|
||||||
|
? JSON.parse(yesterdayCharacter.epithets).join(', ')
|
||||||
|
: (yesterdayCharacter.epithets as string[]).join(', ')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
|
||||||
|
>
|
||||||
|
Voir la page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
Photo
|
Photo
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-white">Placeholder</p>
|
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
|
||||||
<p class="mt-1 text-sm text-slate-200">Revele apres validation de ta tentative.</p>
|
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto">
|
|
||||||
Voir l'archive
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
33
src/routes/daily/+server.ts
Normal file
33
src/routes/daily/+server.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { characterHistory } from '$lib/server/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
try {
|
||||||
|
const { characterId } = await request.json();
|
||||||
|
|
||||||
|
if (!characterId) {
|
||||||
|
return json({ error: 'Missing characterId' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const todayDate = today.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Increment the won counter for today's entry
|
||||||
|
await db
|
||||||
|
.update(characterHistory)
|
||||||
|
.set({
|
||||||
|
won: sql`${characterHistory.won} + 1`,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
|
.where(eq(characterHistory.date, todayDate));
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error recording win:', error);
|
||||||
|
return json({ error: 'Failed to record win' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user