Compare commits

..

19 Commits

Author SHA1 Message Date
2c4730487d feat: update GuessHistoryTable styles for improved layout and readability
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
2026-03-02 20:54:48 +01:00
c40ec7e91a feat: add userCharacterHistory table schema and update journal with new version
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
2026-03-02 20:42:44 +01:00
fc99dc826e feat: add deployment webhook call to build image workflow
All checks were successful
Build Docker Image / build (push) Successful in 1m20s
2026-03-02 20:28:42 +01:00
e11fd63ebb feat: update Docker image build workflow to lowercase repository name in output
All checks were successful
Build Docker Image / build (push) Successful in 1m21s
2026-03-02 20:13:49 +01:00
201c4759b6 feat: remove daily character seed from environment and implement random seed generation for selection 2026-03-02 12:41:18 +01:00
485d96026a feat: implement daily character seed from environment variable and enhance selection logic with hashing 2026-03-02 12:26:11 +01:00
a339498879 feat: refactor character history schema and related logic for improved date handling and data integrity 2026-03-02 12:20:13 +01:00
b5d53d69c1 feat: refactor daily character logic to improve win count retrieval and streamline character selection 2026-03-02 11:50:41 +01:00
f31f49aec7 feat: enhance column visibility toggle with localized display names and improve layout 2026-03-02 11:27:40 +01:00
8aac371c32 feat: implement infinite mode with character selection and scoring; refactor daily character storage logic 2026-03-02 11:23:52 +01:00
c3bc429af2 feat: add WinPanel and YesterdayCharacter components; refactor daily game page
- Introduced WinPanel component to display win/loss messages based on game outcome.
- Added YesterdayCharacter component to show the character from the previous day.
- Refactored daily game page to utilize new components for better code organization and readability.
- Updated server-side logic to fetch daily character ID and count wins more efficiently.
- Cleaned up character selection logic and hint availability tracking.
2026-03-02 11:05:02 +01:00
7157e8c5a6 feat: add promote admin script to manage user roles 2026-03-01 23:38:39 +01:00
bbce1ff136 feat: improve character update logic and handle haki fields more effectively 2026-03-01 23:35:20 +01:00
a80e977e87 fix: adjust main component height for consistent layout across game, login, and profile pages 2026-03-01 23:30:53 +01:00
b4aa5e1a73 feat: enhance admin layout with navigation and return link; add name field for sign-up in login 2026-03-01 23:08:28 +01:00
b849e6c4dc feat: add daily win recording endpoint, user authentication, and profile management
- Implemented a POST endpoint for recording daily wins in the game.
- Created login and signup functionality with email and password.
- Developed a profile page allowing users to update their profile information, change passwords, and manage active sessions.
- Added a toggle feature for switching between login and signup forms.
- Enhanced the layout by removing the profile button and adjusting the header structure.
2026-03-01 23:01:44 +01:00
40bdc80773 fix: update @types/node version and improve type handling in daily character affiliations 2026-03-01 20:23:28 +01:00
b183b5877b feat: add user admin status and profile management
- Updated user schema to include isAdmin field.
- Enhanced authentication hooks to fetch and set user admin status.
- Created ProfileButton component for user profile actions.
- Implemented profile and password update functionality.
- Added session management for user accounts.
- Developed login and signup pages with form handling.
- Introduced layout server for user session data.
- Updated daily page to reflect character changes.
2026-03-01 19:52:54 +01:00
ce9ffd2736 feat: add new character image for Rodriguez Zoro 2026-03-01 17:54:05 +01:00
61 changed files with 4145 additions and 6997 deletions

View File

@@ -7,10 +7,3 @@ ORIGIN=""
# For production use 32 characters and generated with high entropy # For production use 32 characters and generated with high entropy
# https://www.better-auth.com/docs/installation # https://www.better-auth.com/docs/installation
BETTER_AUTH_SECRET="" BETTER_AUTH_SECRET=""
# SvelteKit request body limit (default is 512K)
# Increase this for image uploads (e.g. 5M, 10M)
BODY_SIZE_LIMIT="10M"
# Uploaded files directory (relative to project root)
UPLOADS_DIR="uploads"

3
.gitignore vendored
View File

@@ -27,6 +27,3 @@ vite.config.ts.timestamp-*
# Script outputs # Script outputs
/scraped-data /scraped-data
# Uploads
/uploads

View File

@@ -0,0 +1,156 @@
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` (
`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 DEFAULT 0,
`height` real,
`origin` text,
`firstAppearance` integer NOT NULL,
`pictureUrl` text,
`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
CREATE TABLE `characterHistory` (
`id` text PRIMARY KEY NOT NULL,
`characterId` text,
`date` text,
`won` integer DEFAULT 0 NOT NULL,
`createdAt` integer NOT NULL,
`updatedAt` integer NOT NULL,
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
);
--> 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` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`type` text,
`url` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `devilFruit_name_unique` ON `devilFruit` (`name`);--> statement-breakpoint
CREATE TABLE `account` (
`id` text PRIMARY KEY NOT NULL,
`account_id` text NOT NULL,
`provider_id` text NOT NULL,
`user_id` text NOT NULL,
`access_token` text,
`refresh_token` text,
`id_token` text,
`access_token_expires_at` integer,
`refresh_token_expires_at` integer,
`scope` text,
`password` text,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL,
`token` text NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
`ip_address` text,
`user_agent` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`email_verified` integer DEFAULT false NOT NULL,
`image` text,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verification` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);

View File

@@ -1,199 +0,0 @@
CREATE TABLE `arc` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`fr_name` text,
`start_chapter` integer NOT NULL,
`end_chapter` integer,
`url` text
);
--> statement-breakpoint
CREATE TABLE `character` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`fr_name` text,
`gender` text,
`age` integer,
`affiliations` text,
`fr_affiliations` text,
`devil_fruit_id` text,
`haki_observation` integer DEFAULT false,
`haki_armament` integer DEFAULT false,
`haki_conqueror` integer DEFAULT false,
`bounty` integer DEFAULT 0,
`height` real,
`origin` text,
`fr_origin` text,
`first_appearance` integer NOT NULL,
`picture_url` text,
`epithets` text,
`fr_epithets` text,
`status` text,
`arc_id` text,
`url` text,
`fr_url` text,
`is_in_daily_mode` integer DEFAULT false,
FOREIGN KEY (`devil_fruit_id`) REFERENCES `devil_fruit`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`arc_id`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE TABLE `character_history` (
`id` text PRIMARY KEY NOT NULL,
`character_id` text,
`date` integer NOT NULL,
`won` integer DEFAULT 0 NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`character_id`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `character_history_date_unique` ON `character_history` (`date`);--> statement-breakpoint
CREATE TABLE `character_override` (
`character_id` text PRIMARY KEY NOT NULL,
`name` text,
`gender` text,
`age` integer,
`affiliations` text,
`fr_affiliations` text,
`devil_fruit_id` text,
`haki_observation` integer,
`haki_armament` integer,
`haki_conqueror` integer,
`bounty` integer,
`height` real,
`origin` text,
`fr_origin` text,
`first_appearance` integer,
`picture_url` text,
`epithets` text,
`fr_epithets` text,
`status` text,
`arc_id` text,
`url` text,
`fr_url` text,
`notes` text,
FOREIGN KEY (`character_id`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`devil_fruit_id`) REFERENCES `devil_fruit`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`arc_id`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE TABLE `character_scrape_validation` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`fr_name` text,
`gender` text,
`age` integer,
`affiliations` text,
`fr_affiliations` text,
`devil_fruit_id` text,
`haki_observation` integer DEFAULT false,
`haki_armament` integer DEFAULT false,
`haki_conqueror` integer DEFAULT false,
`bounty` integer,
`height` real,
`origin` text,
`fr_origin` text,
`first_appearance` integer NOT NULL,
`picture_url` text,
`epithets` text,
`fr_epithets` text,
`status` text,
`arc_id` text,
`url` text,
`fr_url` text,
FOREIGN KEY (`devil_fruit_id`) REFERENCES `devil_fruit`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`arc_id`) REFERENCES `arc`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE TABLE `config` (
`key` text PRIMARY KEY NOT NULL,
`value` text
);
--> statement-breakpoint
CREATE TABLE `devil_fruit` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`type` text,
`url` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `devil_fruit_name_unique` ON `devil_fruit` (`name`);--> statement-breakpoint
CREATE TABLE `friendship` (
`id` text PRIMARY KEY NOT NULL,
`requester_id` text NOT NULL,
`addressee_id` text NOT NULL,
`status` text DEFAULT 'pending' NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`requester_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`addressee_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `friendship_requester_id_addressee_id_unique` ON `friendship` (`requester_id`,`addressee_id`);--> statement-breakpoint
CREATE TABLE `user_character_history` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text,
`character_history_id` text,
`try_count` integer NOT NULL,
`tried_character_ids` text,
`created_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`character_history_id`) REFERENCES `character_history`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_character_history_user_id_character_history_id_unique` ON `user_character_history` (`user_id`,`character_history_id`);--> statement-breakpoint
CREATE TABLE `account` (
`id` text PRIMARY KEY NOT NULL,
`account_id` text NOT NULL,
`provider_id` text NOT NULL,
`user_id` text NOT NULL,
`access_token` text,
`refresh_token` text,
`id_token` text,
`access_token_expires_at` integer,
`refresh_token_expires_at` integer,
`scope` text,
`password` text,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL,
`token` text NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
`ip_address` text,
`user_agent` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`username` text NOT NULL,
`email` text NOT NULL,
`email_verified` integer DEFAULT false NOT NULL,
`image` text,
`is_admin` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verification` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);

View File

@@ -1 +0,0 @@
ALTER TABLE `character_scrape_validation` ADD `is_deleted` integer DEFAULT false;

View File

@@ -0,0 +1,30 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_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,
`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
INSERT INTO `__new_characterOverride`("characterId", "name", "gender", "age", "affiliations", "devilFruitId", "hakiObservation", "hakiArmament", "hakiConqueror", "bounty", "height", "origin", "firstAppearance", "pictureUrl", "epithets", "status", "arcId", "url", "notes") SELECT "characterId", "name", "gender", "age", "affiliations", "devilFruitId", "hakiObservation", "hakiArmament", "hakiConqueror", "bounty", "height", "origin", "firstAppearance", "pictureUrl", "epithets", "status", "arcId", "url", "notes" FROM `characterOverride`;--> statement-breakpoint
DROP TABLE `characterOverride`;--> statement-breakpoint
ALTER TABLE `__new_characterOverride` RENAME TO `characterOverride`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1 @@
ALTER TABLE `user` ADD `is_admin` integer DEFAULT false NOT NULL;

View File

@@ -1 +0,0 @@
DROP TABLE `character_override`;

View File

@@ -1,8 +0,0 @@
ALTER TABLE `character` ADD `affiliation` text;--> statement-breakpoint
ALTER TABLE `character` ADD `fr_affiliation` text;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `affiliations`;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `fr_affiliations`;--> statement-breakpoint
ALTER TABLE `character_scrape_validation` ADD `affiliation` text;--> statement-breakpoint
ALTER TABLE `character_scrape_validation` ADD `fr_affiliation` text;--> statement-breakpoint
ALTER TABLE `character_scrape_validation` DROP COLUMN `affiliations`;--> statement-breakpoint
ALTER TABLE `character_scrape_validation` DROP COLUMN `fr_affiliations`;

View File

@@ -0,0 +1,16 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_characterHistory` (
`id` text PRIMARY KEY NOT NULL,
`characterId` text,
`date` integer NOT NULL,
`won` integer DEFAULT 0 NOT NULL,
`createdAt` integer NOT NULL,
`updatedAt` integer NOT NULL,
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
INSERT INTO `__new_characterHistory`("id", "characterId", "date", "won", "createdAt", "updatedAt") SELECT "id", "characterId", "date", "won", "createdAt", "updatedAt" FROM `characterHistory`;--> statement-breakpoint
DROP TABLE `characterHistory`;--> statement-breakpoint
ALTER TABLE `__new_characterHistory` RENAME TO `characterHistory`;--> statement-breakpoint
PRAGMA foreign_keys=ON;--> statement-breakpoint
CREATE UNIQUE INDEX `characterHistory_date_unique` ON `characterHistory` (`date`);

View File

@@ -0,0 +1,9 @@
CREATE TABLE `userCharacterHistory` (
`id` text PRIMARY KEY NOT NULL,
`userId` text,
`characterId` text,
`tryCount` integer NOT NULL,
`createdAt` integer NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
);

View File

@@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "4b4f14a1-b37b-44f4-aed3-7289bd8cb6a0", "id": "d1237d76-8f1c-4721-b8dd-d31082ed7b9a",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"arc": { "arc": {
@@ -21,22 +21,15 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": { "startChapter": {
"name": "fr_name", "name": "startChapter",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"start_chapter": {
"name": "start_chapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"end_chapter": { "endChapter": {
"name": "end_chapter", "name": "endChapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -73,13 +66,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -101,38 +87,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -161,22 +140,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -189,13 +161,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -203,8 +168,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -217,30 +182,23 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": { "isInDailyMode": {
"name": "fr_url", "name": "isInDailyMode",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_in_daily_mode": {
"name": "is_in_daily_mode",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": true
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_devil_fruit_id_devil_fruit_id_fk": { "character_devilFruitId_devilFruit_id_fk": {
"name": "character_devil_fruit_id_devil_fruit_id_fk", "name": "character_devilFruitId_devilFruit_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
@@ -248,17 +206,17 @@
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_arc_id_arc_id_fk": { "character_arcId_arc_id_fk": {
"name": "character_arc_id_arc_id_fk", "name": "character_arcId_arc_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -266,8 +224,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_history": { "characterHistory": {
"name": "character_history", "name": "characterHistory",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -276,8 +234,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -285,9 +243,9 @@
}, },
"date": { "date": {
"name": "date", "name": "date",
"type": "integer", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"won": { "won": {
@@ -298,42 +256,34 @@
"autoincrement": false, "autoincrement": false,
"default": 0 "default": 0
}, },
"created_at": { "createdAt": {
"name": "created_at", "name": "createdAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"updated_at": { "updatedAt": {
"name": "updated_at", "name": "updatedAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
} }
}, },
"indexes": { "indexes": {},
"character_history_date_unique": {
"name": "character_history_date_unique",
"columns": [
"date"
],
"isUnique": true
}
},
"foreignKeys": { "foreignKeys": {
"character_history_character_id_character_id_fk": { "characterHistory_characterId_character_id_fk": {
"name": "character_history_character_id_character_id_fk", "name": "characterHistory_characterId_character_id_fk",
"tableFrom": "character_history", "tableFrom": "characterHistory",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -341,11 +291,11 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_override": { "characterOverride": {
"name": "character_override", "name": "characterOverride",
"columns": { "columns": {
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true,
@@ -379,36 +329,29 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -435,22 +378,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -463,13 +399,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -477,8 +406,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -491,13 +420,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": { "notes": {
"name": "notes", "name": "notes",
"type": "text", "type": "text",
@@ -508,43 +430,43 @@
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_override_character_id_character_id_fk": { "characterOverride_characterId_character_id_fk": {
"name": "character_override_character_id_character_id_fk", "name": "characterOverride_characterId_character_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_override_devil_fruit_id_devil_fruit_id_fk": { "characterOverride_devilFruitId_devilFruit_id_fk": {
"name": "character_override_devil_fruit_id_devil_fruit_id_fk", "name": "characterOverride_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_override_arc_id_arc_id_fk": { "characterOverride_arcId_arc_id_fk": {
"name": "character_override_arc_id_arc_id_fk", "name": "characterOverride_arcId_arc_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -552,8 +474,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_scrape_validation": { "characterScrapeValidation": {
"name": "character_scrape_validation", "name": "characterScrapeValidation",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -569,13 +491,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -597,38 +512,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -656,22 +564,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -684,13 +585,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -698,8 +592,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -711,41 +605,34 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_scrape_validation_devil_fruit_id_devil_fruit_id_fk": { "characterScrapeValidation_devilFruitId_devilFruit_id_fk": {
"name": "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk", "name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_scrape_validation_arc_id_arc_id_fk": { "characterScrapeValidation_arcId_arc_id_fk": {
"name": "character_scrape_validation_arc_id_arc_id_fk", "name": "characterScrapeValidation_arcId_arc_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -777,8 +664,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"devil_fruit": { "devilFruit": {
"name": "devil_fruit", "name": "devilFruit",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -810,8 +697,8 @@
} }
}, },
"indexes": { "indexes": {
"devil_fruit_name_unique": { "devilFruit_name_unique": {
"name": "devil_fruit_name_unique", "name": "devilFruit_name_unique",
"columns": [ "columns": [
"name" "name"
], ],
@@ -823,183 +710,6 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"friendship": {
"name": "friendship",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"requester_id": {
"name": "requester_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"addressee_id": {
"name": "addressee_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"friendship_requester_id_addressee_id_unique": {
"name": "friendship_requester_id_addressee_id_unique",
"columns": [
"requester_id",
"addressee_id"
],
"isUnique": true
}
},
"foreignKeys": {
"friendship_requester_id_user_id_fk": {
"name": "friendship_requester_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"requester_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"friendship_addressee_id_user_id_fk": {
"name": "friendship_addressee_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"addressee_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_character_history": {
"name": "user_character_history",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character_history_id": {
"name": "character_history_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"try_count": {
"name": "try_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tried_character_ids": {
"name": "tried_character_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_character_history_user_id_character_history_id_unique": {
"name": "user_character_history_user_id_character_history_id_unique",
"columns": [
"user_id",
"character_history_id"
],
"isUnique": true
}
},
"foreignKeys": {
"user_character_history_user_id_user_id_fk": {
"name": "user_character_history_user_id_user_id_fk",
"tableFrom": "user_character_history",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_character_history_character_history_id_character_history_id_fk": {
"name": "user_character_history_character_history_id_character_history_id_fk",
"tableFrom": "user_character_history",
"tableTo": "character_history",
"columnsFrom": [
"character_history_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": { "account": {
"name": "account", "name": "account",
"columns": { "columns": {
@@ -1237,13 +947,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@@ -1266,14 +969,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"is_admin": {
"name": "is_admin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "integer", "type": "integer",
@@ -1292,13 +987,6 @@
} }
}, },
"indexes": { "indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"columns": [ "columns": [

View File

@@ -1,8 +1,8 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "9a965dd1-d97c-4142-a795-0558214180a4", "id": "23b693a1-eebd-499e-9755-27a732e1afc1",
"prevId": "4b4f14a1-b37b-44f4-aed3-7289bd8cb6a0", "prevId": "d1237d76-8f1c-4721-b8dd-d31082ed7b9a",
"tables": { "tables": {
"arc": { "arc": {
"name": "arc", "name": "arc",
@@ -21,22 +21,15 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": { "startChapter": {
"name": "fr_name", "name": "startChapter",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"start_chapter": {
"name": "start_chapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"end_chapter": { "endChapter": {
"name": "end_chapter", "name": "endChapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -73,13 +66,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -101,38 +87,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -161,22 +140,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -189,13 +161,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -203,8 +168,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -217,30 +182,23 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": { "isInDailyMode": {
"name": "fr_url", "name": "isInDailyMode",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_in_daily_mode": {
"name": "is_in_daily_mode",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": true
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_devil_fruit_id_devil_fruit_id_fk": { "character_devilFruitId_devilFruit_id_fk": {
"name": "character_devil_fruit_id_devil_fruit_id_fk", "name": "character_devilFruitId_devilFruit_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
@@ -248,17 +206,17 @@
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_arc_id_arc_id_fk": { "character_arcId_arc_id_fk": {
"name": "character_arc_id_arc_id_fk", "name": "character_arcId_arc_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -266,8 +224,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_history": { "characterHistory": {
"name": "character_history", "name": "characterHistory",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -276,8 +234,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -285,9 +243,9 @@
}, },
"date": { "date": {
"name": "date", "name": "date",
"type": "integer", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"won": { "won": {
@@ -298,42 +256,34 @@
"autoincrement": false, "autoincrement": false,
"default": 0 "default": 0
}, },
"created_at": { "createdAt": {
"name": "created_at", "name": "createdAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"updated_at": { "updatedAt": {
"name": "updated_at", "name": "updatedAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
} }
}, },
"indexes": { "indexes": {},
"character_history_date_unique": {
"name": "character_history_date_unique",
"columns": [
"date"
],
"isUnique": true
}
},
"foreignKeys": { "foreignKeys": {
"character_history_character_id_character_id_fk": { "characterHistory_characterId_character_id_fk": {
"name": "character_history_character_id_character_id_fk", "name": "characterHistory_characterId_character_id_fk",
"tableFrom": "character_history", "tableFrom": "characterHistory",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -341,11 +291,11 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_override": { "characterOverride": {
"name": "character_override", "name": "characterOverride",
"columns": { "columns": {
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true,
@@ -379,36 +329,29 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -435,22 +378,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -463,13 +399,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -477,8 +406,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -491,13 +420,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": { "notes": {
"name": "notes", "name": "notes",
"type": "text", "type": "text",
@@ -508,43 +430,43 @@
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_override_character_id_character_id_fk": { "characterOverride_characterId_character_id_fk": {
"name": "character_override_character_id_character_id_fk", "name": "characterOverride_characterId_character_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_override_devil_fruit_id_devil_fruit_id_fk": { "characterOverride_devilFruitId_devilFruit_id_fk": {
"name": "character_override_devil_fruit_id_devil_fruit_id_fk", "name": "characterOverride_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_override_arc_id_arc_id_fk": { "characterOverride_arcId_arc_id_fk": {
"name": "character_override_arc_id_arc_id_fk", "name": "characterOverride_arcId_arc_id_fk",
"tableFrom": "character_override", "tableFrom": "characterOverride",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -552,8 +474,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_scrape_validation": { "characterScrapeValidation": {
"name": "character_scrape_validation", "name": "characterScrapeValidation",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -569,13 +491,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -597,38 +512,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -656,22 +564,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -684,13 +585,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -698,8 +592,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -711,49 +605,34 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_deleted": {
"name": "is_deleted",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_scrape_validation_devil_fruit_id_devil_fruit_id_fk": { "characterScrapeValidation_devilFruitId_devilFruit_id_fk": {
"name": "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk", "name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_scrape_validation_arc_id_arc_id_fk": { "characterScrapeValidation_arcId_arc_id_fk": {
"name": "character_scrape_validation_arc_id_arc_id_fk", "name": "characterScrapeValidation_arcId_arc_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -785,8 +664,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"devil_fruit": { "devilFruit": {
"name": "devil_fruit", "name": "devilFruit",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -818,8 +697,8 @@
} }
}, },
"indexes": { "indexes": {
"devil_fruit_name_unique": { "devilFruit_name_unique": {
"name": "devil_fruit_name_unique", "name": "devilFruit_name_unique",
"columns": [ "columns": [
"name" "name"
], ],
@@ -831,183 +710,6 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"friendship": {
"name": "friendship",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"requester_id": {
"name": "requester_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"addressee_id": {
"name": "addressee_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"friendship_requester_id_addressee_id_unique": {
"name": "friendship_requester_id_addressee_id_unique",
"columns": [
"requester_id",
"addressee_id"
],
"isUnique": true
}
},
"foreignKeys": {
"friendship_requester_id_user_id_fk": {
"name": "friendship_requester_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"requester_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"friendship_addressee_id_user_id_fk": {
"name": "friendship_addressee_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"addressee_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_character_history": {
"name": "user_character_history",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character_history_id": {
"name": "character_history_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"try_count": {
"name": "try_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tried_character_ids": {
"name": "tried_character_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_character_history_user_id_character_history_id_unique": {
"name": "user_character_history_user_id_character_history_id_unique",
"columns": [
"user_id",
"character_history_id"
],
"isUnique": true
}
},
"foreignKeys": {
"user_character_history_user_id_user_id_fk": {
"name": "user_character_history_user_id_user_id_fk",
"tableFrom": "user_character_history",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_character_history_character_history_id_character_history_id_fk": {
"name": "user_character_history_character_history_id_character_history_id_fk",
"tableFrom": "user_character_history",
"tableTo": "character_history",
"columnsFrom": [
"character_history_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": { "account": {
"name": "account", "name": "account",
"columns": { "columns": {
@@ -1245,13 +947,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@@ -1274,14 +969,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"is_admin": {
"name": "is_admin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "integer", "type": "integer",
@@ -1300,13 +987,6 @@
} }
}, },
"indexes": { "indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"columns": [ "columns": [

View File

@@ -1,8 +1,8 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "f3540f13-a6c4-4c52-ac29-6330ffce33fd", "id": "4fa96ce4-93c4-4d2d-9f9a-5badf47dfb05",
"prevId": "9a965dd1-d97c-4142-a795-0558214180a4", "prevId": "23b693a1-eebd-499e-9755-27a732e1afc1",
"tables": { "tables": {
"arc": { "arc": {
"name": "arc", "name": "arc",
@@ -21,22 +21,15 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": { "startChapter": {
"name": "fr_name", "name": "startChapter",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"start_chapter": {
"name": "start_chapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"end_chapter": { "endChapter": {
"name": "end_chapter", "name": "endChapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -73,13 +66,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -101,38 +87,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -161,22 +140,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -189,13 +161,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -203,8 +168,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -217,30 +182,23 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": { "isInDailyMode": {
"name": "fr_url", "name": "isInDailyMode",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_in_daily_mode": {
"name": "is_in_daily_mode",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": true
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_devil_fruit_id_devil_fruit_id_fk": { "character_devilFruitId_devilFruit_id_fk": {
"name": "character_devil_fruit_id_devil_fruit_id_fk", "name": "character_devilFruitId_devilFruit_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
@@ -248,17 +206,17 @@
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_arc_id_arc_id_fk": { "character_arcId_arc_id_fk": {
"name": "character_arc_id_arc_id_fk", "name": "character_arcId_arc_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -266,8 +224,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_history": { "characterHistory": {
"name": "character_history", "name": "characterHistory",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -276,8 +234,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -285,9 +243,9 @@
}, },
"date": { "date": {
"name": "date", "name": "date",
"type": "integer", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"won": { "won": {
@@ -298,42 +256,34 @@
"autoincrement": false, "autoincrement": false,
"default": 0 "default": 0
}, },
"created_at": { "createdAt": {
"name": "created_at", "name": "createdAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"updated_at": { "updatedAt": {
"name": "updated_at", "name": "updatedAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
} }
}, },
"indexes": { "indexes": {},
"character_history_date_unique": {
"name": "character_history_date_unique",
"columns": [
"date"
],
"isUnique": true
}
},
"foreignKeys": { "foreignKeys": {
"character_history_character_id_character_id_fk": { "characterHistory_characterId_character_id_fk": {
"name": "character_history_character_id_character_id_fk", "name": "characterHistory_characterId_character_id_fk",
"tableFrom": "character_history", "tableFrom": "characterHistory",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -341,8 +291,191 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_scrape_validation": { "characterOverride": {
"name": "character_scrape_validation", "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": false,
"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": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -358,13 +491,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -386,38 +512,31 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliations": { "devilFruitId": {
"name": "fr_affiliations", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -445,22 +564,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -473,13 +585,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -487,8 +592,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -500,49 +605,34 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_deleted": {
"name": "is_deleted",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_scrape_validation_devil_fruit_id_devil_fruit_id_fk": { "characterScrapeValidation_devilFruitId_devilFruit_id_fk": {
"name": "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk", "name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_scrape_validation_arc_id_arc_id_fk": { "characterScrapeValidation_arcId_arc_id_fk": {
"name": "character_scrape_validation_arc_id_arc_id_fk", "name": "characterScrapeValidation_arcId_arc_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -574,8 +664,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"devil_fruit": { "devilFruit": {
"name": "devil_fruit", "name": "devilFruit",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -607,8 +697,8 @@
} }
}, },
"indexes": { "indexes": {
"devil_fruit_name_unique": { "devilFruit_name_unique": {
"name": "devil_fruit_name_unique", "name": "devilFruit_name_unique",
"columns": [ "columns": [
"name" "name"
], ],
@@ -620,183 +710,6 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"friendship": {
"name": "friendship",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"requester_id": {
"name": "requester_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"addressee_id": {
"name": "addressee_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"friendship_requester_id_addressee_id_unique": {
"name": "friendship_requester_id_addressee_id_unique",
"columns": [
"requester_id",
"addressee_id"
],
"isUnique": true
}
},
"foreignKeys": {
"friendship_requester_id_user_id_fk": {
"name": "friendship_requester_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"requester_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"friendship_addressee_id_user_id_fk": {
"name": "friendship_addressee_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"addressee_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_character_history": {
"name": "user_character_history",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character_history_id": {
"name": "character_history_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"try_count": {
"name": "try_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tried_character_ids": {
"name": "tried_character_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_character_history_user_id_character_history_id_unique": {
"name": "user_character_history_user_id_character_history_id_unique",
"columns": [
"user_id",
"character_history_id"
],
"isUnique": true
}
},
"foreignKeys": {
"user_character_history_user_id_user_id_fk": {
"name": "user_character_history_user_id_user_id_fk",
"tableFrom": "user_character_history",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_character_history_character_history_id_character_history_id_fk": {
"name": "user_character_history_character_history_id_character_history_id_fk",
"tableFrom": "user_character_history",
"tableTo": "character_history",
"columnsFrom": [
"character_history_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": { "account": {
"name": "account", "name": "account",
"columns": { "columns": {
@@ -1034,13 +947,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@@ -1089,13 +995,6 @@
} }
}, },
"indexes": { "indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"columns": [ "columns": [

View File

@@ -1,8 +1,8 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "736137e1-d840-4f5b-a1b4-d50648839073", "id": "8a8486cf-5e1c-4fcb-94ce-b6967ae10290",
"prevId": "f3540f13-a6c4-4c52-ac29-6330ffce33fd", "prevId": "4fa96ce4-93c4-4d2d-9f9a-5badf47dfb05",
"tables": { "tables": {
"arc": { "arc": {
"name": "arc", "name": "arc",
@@ -21,22 +21,15 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": { "startChapter": {
"name": "fr_name", "name": "startChapter",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"start_chapter": {
"name": "start_chapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"end_chapter": { "endChapter": {
"name": "end_chapter", "name": "endChapter",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -73,13 +66,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"type": "text", "type": "text",
@@ -94,45 +80,38 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"affiliation": { "affiliations": {
"name": "affiliation", "name": "affiliations",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliation": { "devilFruitId": {
"name": "fr_affiliation", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "name": "hakiObservation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haki_observation": {
"name": "haki_observation",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -161,22 +140,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -189,13 +161,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -203,8 +168,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -217,30 +182,23 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_url": { "isInDailyMode": {
"name": "fr_url", "name": "isInDailyMode",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_in_daily_mode": {
"name": "is_in_daily_mode",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": true
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_devil_fruit_id_devil_fruit_id_fk": { "character_devilFruitId_devilFruit_id_fk": {
"name": "character_devil_fruit_id_devil_fruit_id_fk", "name": "character_devilFruitId_devilFruit_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
@@ -248,17 +206,17 @@
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_arc_id_arc_id_fk": { "character_arcId_arc_id_fk": {
"name": "character_arc_id_arc_id_fk", "name": "character_arcId_arc_id_fk",
"tableFrom": "character", "tableFrom": "character",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -266,8 +224,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_history": { "characterHistory": {
"name": "character_history", "name": "characterHistory",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -276,8 +234,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"character_id": { "characterId": {
"name": "character_id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -298,15 +256,15 @@
"autoincrement": false, "autoincrement": false,
"default": 0 "default": 0
}, },
"created_at": { "createdAt": {
"name": "created_at", "name": "createdAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"updated_at": { "updatedAt": {
"name": "updated_at", "name": "updatedAt",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@@ -314,8 +272,8 @@
} }
}, },
"indexes": { "indexes": {
"character_history_date_unique": { "characterHistory_date_unique": {
"name": "character_history_date_unique", "name": "characterHistory_date_unique",
"columns": [ "columns": [
"date" "date"
], ],
@@ -323,17 +281,17 @@
} }
}, },
"foreignKeys": { "foreignKeys": {
"character_history_character_id_character_id_fk": { "characterHistory_characterId_character_id_fk": {
"name": "character_history_character_id_character_id_fk", "name": "characterHistory_characterId_character_id_fk",
"tableFrom": "character_history", "tableFrom": "characterHistory",
"tableTo": "character", "tableTo": "character",
"columnsFrom": [ "columnsFrom": [
"character_id" "characterId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "cascade", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -341,11 +299,11 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"character_scrape_validation": { "characterOverride": {
"name": "character_scrape_validation", "name": "characterOverride",
"columns": { "columns": {
"id": { "characterId": {
"name": "id", "name": "characterId",
"type": "text", "type": "text",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true,
@@ -355,13 +313,6 @@
"name": "name", "name": "name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fr_name": {
"name": "fr_name",
"type": "text",
"primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
@@ -379,45 +330,221 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"affiliation": { "affiliations": {
"name": "affiliation", "name": "affiliations",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_affiliation": { "devilFruitId": {
"name": "fr_affiliation", "name": "devilFruitId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"devil_fruit_id": { "hakiObservation": {
"name": "devil_fruit_id", "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", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"haki_observation": { "firstAppearance": {
"name": "haki_observation", "name": "firstAppearance",
"type": "integer",
"primaryKey": false,
"notNull": false,
"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", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_armament": { "hakiArmament": {
"name": "haki_armament", "name": "hakiArmament",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"haki_conqueror": { "hakiConqueror": {
"name": "haki_conqueror", "name": "hakiConqueror",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -445,22 +572,15 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_origin": { "firstAppearance": {
"name": "fr_origin", "name": "firstAppearance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_appearance": {
"name": "first_appearance",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"picture_url": { "pictureUrl": {
"name": "picture_url", "name": "pictureUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -473,13 +593,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"fr_epithets": {
"name": "fr_epithets",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "text", "type": "text",
@@ -487,8 +600,8 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"arc_id": { "arcId": {
"name": "arc_id", "name": "arcId",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@@ -500,49 +613,34 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"fr_url": {
"name": "fr_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_deleted": {
"name": "is_deleted",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"character_scrape_validation_devil_fruit_id_devil_fruit_id_fk": { "characterScrapeValidation_devilFruitId_devilFruit_id_fk": {
"name": "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk", "name": "characterScrapeValidation_devilFruitId_devilFruit_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "devil_fruit", "tableTo": "devilFruit",
"columnsFrom": [ "columnsFrom": [
"devil_fruit_id" "devilFruitId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"character_scrape_validation_arc_id_arc_id_fk": { "characterScrapeValidation_arcId_arc_id_fk": {
"name": "character_scrape_validation_arc_id_arc_id_fk", "name": "characterScrapeValidation_arcId_arc_id_fk",
"tableFrom": "character_scrape_validation", "tableFrom": "characterScrapeValidation",
"tableTo": "arc", "tableTo": "arc",
"columnsFrom": [ "columnsFrom": [
"arc_id" "arcId"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "set null", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
@@ -574,8 +672,8 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"devil_fruit": { "devilFruit": {
"name": "devil_fruit", "name": "devilFruit",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
@@ -607,8 +705,8 @@
} }
}, },
"indexes": { "indexes": {
"devil_fruit_name_unique": { "devilFruit_name_unique": {
"name": "devil_fruit_name_unique", "name": "devilFruit_name_unique",
"columns": [ "columns": [
"name" "name"
], ],
@@ -620,183 +718,6 @@
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
}, },
"friendship": {
"name": "friendship",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"requester_id": {
"name": "requester_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"addressee_id": {
"name": "addressee_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"friendship_requester_id_addressee_id_unique": {
"name": "friendship_requester_id_addressee_id_unique",
"columns": [
"requester_id",
"addressee_id"
],
"isUnique": true
}
},
"foreignKeys": {
"friendship_requester_id_user_id_fk": {
"name": "friendship_requester_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"requester_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"friendship_addressee_id_user_id_fk": {
"name": "friendship_addressee_id_user_id_fk",
"tableFrom": "friendship",
"tableTo": "user",
"columnsFrom": [
"addressee_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_character_history": {
"name": "user_character_history",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character_history_id": {
"name": "character_history_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"try_count": {
"name": "try_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tried_character_ids": {
"name": "tried_character_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_character_history_user_id_character_history_id_unique": {
"name": "user_character_history_user_id_character_history_id_unique",
"columns": [
"user_id",
"character_history_id"
],
"isUnique": true
}
},
"foreignKeys": {
"user_character_history_user_id_user_id_fk": {
"name": "user_character_history_user_id_user_id_fk",
"tableFrom": "user_character_history",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_character_history_character_history_id_character_history_id_fk": {
"name": "user_character_history_character_history_id_character_history_id_fk",
"tableFrom": "user_character_history",
"tableTo": "character_history",
"columnsFrom": [
"character_history_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": { "account": {
"name": "account", "name": "account",
"columns": { "columns": {
@@ -1034,13 +955,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@@ -1089,13 +1003,6 @@
} }
}, },
"indexes": { "indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"columns": [ "columns": [

File diff suppressed because it is too large Load Diff

View File

@@ -5,29 +5,36 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1773602933375, "when": 1772325597983,
"tag": "0000_huge_doctor_octopus", "tag": "0000_graceful_master_mold",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1773697753818, "when": 1772383366179,
"tag": "0001_fuzzy_talisman", "tag": "0001_nostalgic_hercules",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "6", "version": "6",
"when": 1775950314114, "when": 1772390182445,
"tag": "0002_old_earthquake", "tag": "0002_large_gwen_stacy",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "6", "version": "6",
"when": 1776195681488, "when": 1772449624450,
"tag": "0003_mixed_ben_grimm", "tag": "0003_wise_blonde_phantom",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1772480377099,
"tag": "0004_unique_lorna_dane",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,255 +1,144 @@
[ [
"absalom_absalom", "aladdin_aladdin",
"king_king", "alvida_alvida",
"alvida_alvida", "aramaki_aramaki",
"aramaki_aramaki", "arlong_arlong",
"arlong_arlong", "ashura_doji_ashura_doji",
"ashura_doji_ashura_doji", "baby_5_baby_5",
"vegapunk/atlas_atlas", "baggy_baggy",
"avalo_pizarro_avalo_pizarro", "bartholomew_kuma_bartholomew_kuma",
"baby_5_baby_5", "bartolomeo_bartolomeo",
"buggy_buggy", "basil_hawkins_basil_hawkins",
"bartholomew_kuma_bartholomew_kuma", "batman_batman",
"bartolomeo_bartolomeo", "bellamy_bellamy",
"basil_hawkins_basil_hawkins", "belo_betty_belo_betty",
"bell-mère_bell-mère", "ben_beckman_ben_beckman",
"bellamy_bellamy", "bentham_bentham",
"belo_betty_belo_betty", "bepo_bepo",
"benn_beckman_ben_beckman", "black_maria_black_maria",
"bentham_bentham", "boa_hancock_boa_hancock",
"bepo_bepo", "boa_marigold_boa_marigold",
"black_maria_black_maria", "boa_sandersonia_boa_sandersonia",
"blueno_blueno", "borsalino_borsalino",
"boa_hancock_boa_hancock", "brogy_brogy",
"boa_marigold_boa_marigold", "brook_brook",
"boa_sandersonia_boa_sandersonia", "camie_camie",
"borsalino_borsalino", "capone_bege_capone_bege",
"brogy_brogy", "caribou_caribou",
"brook_brook", "carrot_carrot",
"buckingham_stussy_buckingham_stussy", "catarina_devon_catarina_devon",
"buffalo_buffalo", "cavendish_cavendish",
"camie_camie", "cesar_clown_cesar_clown",
"capone_bege_capone_bege", "chinjao_chinjao",
"carmel_carmel", "coby_coby",
"caribou_caribou", "corazon_corazon",
"carrot_carrot", "crocodile_crocodile",
"catarina_devon_catarina_devon", "crocus_crocus",
"cavendish_cavendish", "curly_dadan_curly_dadan",
"caesar_clown_caesar_clown", "dalton_dalton",
"charlotte_brûlée_charlotte_brûlée", "daz_bones_daz_bones",
"charlotte_cracker_charlotte_cracker", "denjiro_denjiro",
"charlotte_katakuri_charlotte_katakuri", "diamante_diamante",
"charlotte_linlin_charlotte_linlin", "doc_q_doc_q",
"charlotte_mont-d'or_charlotte_mont-d'or", "don_quichotte_doflamingo_don_quichotte_doflamingo",
"charlotte_oven_charlotte_oven", "don_quichotte_rossinante_don_quichotte_rossinante",
"charlotte_perospero_charlotte_perospero", "dorry_dorry",
"charlotte_pudding_charlotte_pudding", "dracule_mihawk_dracule_mihawk",
"charlotte_smoothie_charlotte_smoothie", "duval_duval",
"chinjao_chinjao", "edward_newgate_edward_newgate",
"clou_d_clover_clou_d_clover", "edward_weevil_edward_weevil",
"crocodile_crocodile", "emporio_ivankov_emporio_ivankov",
"crocus_crocus", "enel_enel",
"curly_dadan_curly_dadan", "eustass_kid_eustass_kid",
"dalton_dalton", "fisher_tiger_fisher_tiger",
"daz_bonez_daz_bonez", "foxy_foxy",
"denjiro_denjiro", "franky_franky",
"diamante_diamante", "fujitora_fujitora",
"doc_q_doc_q", "gan_forr_gan_forr",
"donquixote_doflamingo_donquixote_doflamingo", "gecko_moria_gecko_moria",
"donquixote_rosinante_donquixote_rosinante", "gin_gin",
"dorry_dorry", "gol_d_roger_gol_d_roger",
"dracule_mihawk_dracule_mihawk", "haguar_d_sauro_haguar_d_sauro",
"vegapunk/edison_edison", "hajrudin_hajrudin",
"edward_newgate_edward_newgate", "hannyabal_hannyabal",
"edward_weevil_edward_weevil", "hatchan_hatchan",
"emporio_ivankov_emporio_ivankov", "hina_hina",
"enel_enel", "hody_jones_hody_jones",
"eustass_kid_eustass_kid", "hyogoro_hyogoro",
"fisher_tiger_fisher_tiger", "iceburg_iceburg",
"foxy_foxy", "imu_imu",
"franky_franky", "inazuma_inazuma",
"fukaboshi_fukaboshi", "inuarashi_inuarashi",
"fukurou_fukurou", "issho_issho",
"galdino_galdino", "izo_izo",
"gan_fall_gan_fall", "jabra_jabra",
"gecko_moria_gecko_moria", "jack_jack",
"gem_gem", "jesus_burgess_jesus_burgess",
"genzo_genzo", "jewelry_bonney_jewelry_bonney",
"gin_gin", "jinbei_jinbei",
"ginny_ginny", "joy_boy_joy_boy",
"gol_d_roger_gol_d_roger", "kaidou_kaidou",
"guernika_guernika", "kaku_kaku",
"hack_hack", "kalgara_kalgara",
"jaguar_d_saul_jaguar_d_saul", "kalifa_kalifa",
"hajrudin_hajrudin", "karasu_karasu",
"hannyabal_hannyabal", "karoo_karoo",
"harald_harald", "kawamatsu_kawamatsu",
"haredas_haredas", "kaya_kaya",
"heracles_heracles", "killer_killer",
"helmeppo_helmeppo", "kinemon_kinemon",
"hibari_hibari", "koala_koala",
"hiriluk_hiriluk", "koby_koby",
"hina_hina", "kong_kong",
"hody_jones_hody_jones", "kozuki_hiyori_kozuki_hiyori",
"hogback_hogback", "kozuki_momonosuke_kozuki_momonosuke",
"hyougoro_hyougoro", "kozuki_oden_kozuki_oden",
"iceburg_iceburg", "krieg_krieg",
"igaram_igaram", "kureha_kureha",
"imu_imu", "kuro_kuro",
"inazuma_inazuma", "kurozumi_orochi_kurozumi_orochi",
"inuarashi_inuarashi", "kuzan_kuzan",
"issho_issho", "kyros_kyros",
"izou_izou", "laboon_laboon",
"jabra_jabra", "laffitte_laffitte",
"jack_jack", "lao_g_lao_g",
"jango_jango", "leo_leo",
"jesus_burgess_jesus_burgess", "lindbergh_lindbergh",
"jewelry_bonney_jewelry_bonney", "loki_loki",
"jinbe_jinbe", "lucky_roux_lucky_roux",
"giolla_giolla", "magellan_magellan",
"joy_boy_joy_boy", "makino_makino",
"jozu_jozu", "marco_marco",
"kaidou_kaidou", "marshall_d_teach_marshall_d_teach",
"kaku_kaku", "monkey_d_dragon_monkey_d_dragon",
"kalgara_kalgara", "monkey_d_garp_monkey_d_garp",
"kalifa_kalifa", "monkey_d_luffy_monkey_d_luffy",
"karasu_karasu", "montblanc_norland_montblanc_norland",
"karoo_karoo", "morgans_morgans",
"kawamatsu_kawamatsu", "morley_morley",
"kaya_kaya", "mr_3_mr_3",
"kelly_funk_kelly_funk", "nami_nami",
"kikunojo_kikunojo", "nefertari_cobra_nefertari_cobra",
"killer_killer", "nefertari_vivi_nefertari_vivi",
"kin'emon_kin'emon", "nekomamushi_nekomamushi",
"koala_koala", "neptune_neptune",
"koby_koby", "nico_robin_nico_robin",
"kokoro_kokoro", "oars_oars",
"kouzuki_hiyori_kouzuki_hiyori", "otohime_otohime",
"kouzuki_momonosuke_kouzuki_momonosuke", "page_one_page_one",
"kouzuki_oden_kouzuki_oden", "pandaman_pandaman",
"kouzuki_sukiyaki_kouzuki_sukiyaki", "pekoms_pekoms",
"kouzuki_toki_kouzuki_toki", "pell_pell",
"krieg_krieg", "perona_perona",
"kumadori_kumadori", "pica_pica",
"kureha_kureha", "portgas_d_ace_portgas_d_ace",
"kuro_kuro", "queen_queen",
"kurozumi_kanjuro_kurozumi_kanjuro", "raizo_raizo",
"kurozumi_orochi_kurozumi_orochi", "rebecca_rebecca",
"kurozumi_tama_kurozumi_tama", "rob_lucci_rob_lucci",
"kuzan_kuzan", "rocks_d_xebec_rocks_d_xebec",
"kyros_kyros", "roronoa_zoro_roronoa_zoro",
"laboon_laboon", "sabo_sabo",
"laffitte_laffitte", "vegapunk_vegapunk",
"lao_g_lao_g", "yamato_yamato"
"leo_leo",
"vegapunk/lilith_lilith",
"lindbergh_lindbergh",
"loki_loki",
"lucky_roux_lucky_roux",
"magellan_magellan",
"makino_makino",
"mansherry_mansherry",
"marco_marco",
"marshall_d_teach_marshall_d_teach",
"merry_merry",
"momoo_momoo",
"mocha_mocha",
"monet_monet",
"monkey_d_dragon_monkey_d_dragon",
"monkey_d_garp_monkey_d_garp",
"monkey_d_luffy_monkey_d_luffy",
"mont_blanc_cricket_mont_blanc_cricket",
"mont_blanc_noland_mont_blanc_noland",
"morgans_morgans",
"morgan_morgan",
"morley_morley",
"nami_nami",
"nefertari_cobra_nefertari_cobra",
"nefertari_vivi_nefertari_vivi",
"nekomamushi_nekomamushi",
"neptune_neptune",
"nico_olvia_nico_olvia",
"nico_robin_nico_robin",
"nojiko_nojiko",
"hatchan_hatchan",
"otohime_otohime",
"oars_oars",
"page_one_page_one",
"pandaman_pandaman",
"paulie_paulie",
"pedro_pedro",
"pekoms_pekoms",
"pell_pell",
"perona_perona",
"pica_pica",
"portgas_d_ace_portgas_d_ace",
"vegapunk/pythagoras_pythagoras",
"queen_queen",
"raizo_raizo",
"rebecca_rebecca",
"riku_doldo_iii_riku_doldo_iii",
"rob_lucci_rob_lucci",
"rocks_d_xebec_rocks_d_xebec",
"roronoa_zoro_roronoa_zoro",
"s-bear_s-bear",
"s-hawk_s-hawk",
"s-snake_s-snake",
"sabo_sabo",
"sadi_sadi",
"donquixote_mjosgard_donquixote_mjosgard",
"rimoshifu_killingham_rimoshifu_killingham",
"manmayer_gunko_manmayer_gunko",
"shepherd_sommers_shepherd_sommers",
"sakazuki_sakazuki",
"sanjuan_wolf_sanjuan_wolf",
"sasaki_sasaki",
"scratchmen_apoo_scratchmen_apoo",
"sengoku_sengoku",
"senor_pink_senor_pink",
"sentomaru_sentomaru",
"vegapunk/shaka_shaka",
"shakuyaku_shakuyaku",
"shanks_shanks",
"shiryu_shiryu",
"shimotsuki_kuina_shimotsuki_kuina",
"shimotsuki_yasuie_shimotsuki_yasuie",
"shinobu_shinobu",
"shirahoshi_shirahoshi",
"silvers_rayleigh_silvers_rayleigh",
"smoker_smoker",
"spandam_spandam",
"speed_speed",
"stussy_stussy",
"sugar_sugar",
"tamago_tamago",
"tashigi_tashigi",
"toko_toko",
"tom_tom",
"tony_tony_chopper_tony_tony_chopper",
"trafalgar_d_water_law_trafalgar_d_water_law",
"trebol_trebol",
"tsuru_tsuru",
"ulti_ulti",
"urouge_urouge",
"usopp_usopp",
"uta_uta",
"van_augur_van_augur",
"vander_decken_ix_vander_decken_ix",
"vegapunk_vegapunk",
"vergo_vergo",
"vinsmoke_ichiji_vinsmoke_ichiji",
"vinsmoke_judge_vinsmoke_judge",
"vinsmoke_niji_vinsmoke_niji",
"vinsmoke_reiju_vinsmoke_reiju",
"sanji_sanji",
"vinsmoke_yonji_vinsmoke_yonji",
"viola_viola",
"wadatsumi_wadatsumi",
"wapol_wapol",
"wyper_wyper",
"x_drake_x_drake",
"yamato_yamato",
"yasopp_yasopp",
"vegapunk/york_york",
"zeff_zeff"
] ]

View File

@@ -1,15 +1,12 @@
import { createClient } from '@libsql/client'; import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq, inArray } from 'drizzle-orm'; import { sql, eq } from 'drizzle-orm';
import fs from 'fs'; import fs from 'fs';
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema'; import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
type Status = 'Alive' | 'Dead' | 'Unknown';
type ArcRecord = { type ArcRecord = {
id: string; id: string;
name: string; name: string;
frName?: string | null;
startChapter: number; startChapter: number;
endChapter?: number | null; endChapter?: number | null;
url?: string | null; url?: string | null;
@@ -25,11 +22,9 @@ type DevilFruitRecord = {
type CharacterRecord = { type CharacterRecord = {
id: string; id: string;
name: string; name: string;
frName?: string | null;
gender?: string | null; gender?: string | null;
age?: number | null; age?: number | null;
affiliation?: string | null; affiliations?: string[] | string | null;
frAffiliation?: string | null;
devilFruitId?: string | null; devilFruitId?: string | null;
hakiObservation?: boolean; hakiObservation?: boolean;
hakiArmament?: boolean; hakiArmament?: boolean;
@@ -37,15 +32,12 @@ type CharacterRecord = {
bounty?: number | null; bounty?: number | null;
height?: number | null; height?: number | null;
origin?: string | null; origin?: string | null;
frOrigin?: string | null;
firstAppearance?: number; firstAppearance?: number;
pictureUrl?: string | null; pictureUrl?: string | null;
epithets?: string[] | string | null; epithets?: string[] | string | null;
frEpithets?: string[] | string | null; status?: string | null;
status?: Status | null;
arcId?: string | null; arcId?: string | null;
url?: string | null; url?: string | null;
frUrl?: string | null;
}; };
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db'; const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
@@ -94,7 +86,7 @@ function toJsonArray(value: string[] | string | null | undefined): string[] | nu
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null { function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
if (!value) return null; if (!value) return null;
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Smile' || value === 'Unknown') { if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
return value; return value;
} }
return 'Unknown'; return 'Unknown';
@@ -120,31 +112,62 @@ function transformCharacterData(item: CharacterRecord) {
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
frName: toNullable(item.frName),
gender: toNullable(item.gender), gender: toNullable(item.gender),
age: toNullable(item.age), age: toNullable(item.age),
affiliation: toNullable(item.affiliation), affiliations: toJsonArray(item.affiliations),
frAffiliation: toNullable(item.frAffiliation),
devilFruitId: toNullable(item.devilFruitId), devilFruitId: toNullable(item.devilFruitId),
hakiObservation: !!item.hakiObservation, hakiObservation: !!item.hakiObservation,
hakiArmament: !!item.hakiArmament, hakiArmament: !!item.hakiArmament,
hakiConqueror: !!item.hakiConqueror, hakiConqueror: !!item.hakiConqueror,
bounty: item.bounty ?? 0, bounty: item.bounty ?? 0,
height: toNumber(item.height as string | number | null), height: toNumber(item.height as any),
origin: toNullable(item.origin), origin: toNullable(item.origin),
frOrigin: toNullable(item.frOrigin),
firstAppearance: item.firstAppearance ?? 0, firstAppearance: item.firstAppearance ?? 0,
pictureUrl: toNullable(item.pictureUrl), pictureUrl: toNullable(item.pictureUrl),
epithets: toJsonArray(item.epithets), epithets: toJsonArray(item.epithets),
frEpithets: toJsonArray(item.frEpithets),
status: toNullable(item.status), status: toNullable(item.status),
arcId: toNullable(item.arcId), arcId: toNullable(item.arcId),
url: toNullable(item.url), url: toNullable(item.url)
frUrl: toNullable(item.frUrl),
isDeleted: false
}; };
} }
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> { async function isCharacterTableEmpty(): Promise<boolean> {
const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character); const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character);
return result[0]?.count === 0; return result[0]?.count === 0;
@@ -172,7 +195,6 @@ async function importFromJson(): Promise<void> {
.values({ .values({
id: item.id, id: item.id,
name: item.name, name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter, startChapter: item.startChapter,
endChapter: toNullable(item.endChapter), endChapter: toNullable(item.endChapter),
url: toNullable(item.url) url: toNullable(item.url)
@@ -181,7 +203,6 @@ async function importFromJson(): Promise<void> {
target: arc.id, target: arc.id,
set: { set: {
name: item.name, name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter, startChapter: item.startChapter,
endChapter: toNullable(item.endChapter), endChapter: toNullable(item.endChapter),
url: toNullable(item.url) url: toNullable(item.url)
@@ -306,9 +327,8 @@ async function importFromJson(): Promise<void> {
} }
} }
} else { } else {
// Update scrapeValidation table // Check for changes and update scrapeValidation table
console.log('Characters table not empty, updating scrapeValidation table for changes...\n'); console.log('Characters table not empty, checking for changes...\n');
const scrapedCharacterIds: string[] = [];
for (let i = 0; i < characters.length; i++) { for (let i = 0; i < characters.length; i++) {
const item = characters[i]; const item = characters[i];
@@ -320,20 +340,33 @@ async function importFromJson(): Promise<void> {
.where(eq(character.id, item.id)); .where(eq(character.id, item.id));
lastSql = selectQuery.toSQL(); lastSql = selectQuery.toSQL();
const [dbCharacter] = await selectQuery;
scrapedCharacterIds.push(item.id);
const jsonData = transformCharacterData(item); const jsonData = transformCharacterData(item);
const changed = hasChanged(jsonData, dbCharacter);
const upsertQuery = db if (changed) {
.insert(characterScrapeValidation) // Update scrapeValidation table with changes
.values(jsonData) const upsertQuery = db
.onConflictDoUpdate({ .insert(characterScrapeValidation)
target: characterScrapeValidation.id, .values(jsonData)
set: 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;
}
lastSql = upsertQuery.toSQL();
await upsertQuery;
successCount++; successCount++;
process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`); process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`);
} catch (error) { } catch (error) {
@@ -344,57 +377,6 @@ async function importFromJson(): Promise<void> {
logSqlOnError(lastSql); logSqlOnError(lastSql);
} }
} }
// Fetch all characters from the character table and mark those absent from the
// scrape as deleted in scrape validation.
const allCharacters = await db.select({ id: character.id }).from(character);
const scrapedSet = new Set(scrapedCharacterIds);
const idsToMarkDeleted = allCharacters
.map((c) => c.id)
.filter((id) => !scrapedSet.has(id));
if (idsToMarkDeleted.length > 0) {
console.log(`\n⚠ Marking ${idsToMarkDeleted.length} character(s) as deleted in scrape validation...`);
const deletedCharacterRows = await db
.select()
.from(character)
.where(inArray(character.id, idsToMarkDeleted));
for (const row of deletedCharacterRows) {
await db
.insert(characterScrapeValidation)
.values({
id: row.id,
name: row.name,
frName: row.frName,
gender: row.gender,
age: row.age,
affiliation: row.affiliation,
frAffiliation: row.frAffiliation,
devilFruitId: row.devilFruitId,
hakiObservation: row.hakiObservation,
hakiArmament: row.hakiArmament,
hakiConqueror: row.hakiConqueror,
bounty: row.bounty,
height: row.height,
origin: row.origin,
frOrigin: row.frOrigin,
firstAppearance: row.firstAppearance,
pictureUrl: row.pictureUrl,
epithets: row.epithets,
frEpithets: row.frEpithets,
status: row.status,
arcId: row.arcId,
url: row.url,
frUrl: row.frUrl,
isDeleted: true
})
.onConflictDoUpdate({
target: characterScrapeValidation.id,
set: { isDeleted: true }
});
}
}
} }
console.log(`\n\n✓ Characters imported!`); console.log(`\n\n✓ Characters imported!`);

View File

@@ -23,8 +23,7 @@ const columns = [
'origin', 'origin',
'devilFruitType', 'devilFruitType',
'arc', 'arc',
'status', 'status'
'age'
] as const; ] as const;
async function initColumnConfig(): Promise<void> { async function initColumnConfig(): Promise<void> {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { createClient } from '@libsql/client'; import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/libsql';
import { eq, inArray } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import fs from 'fs'; import fs from 'fs';
import { character, characterHistory } from '../src/lib/server/db/schema'; import { character, characterHistory } from '../src/lib/server/db/schema';
@@ -24,14 +24,13 @@ function getErrorMessage(error: unknown): string {
async function setDailyCharacters(): Promise<void> { async function setDailyCharacters(): Promise<void> {
try { try {
const dailyCharacterIdsRaw = readJsonFile('./scripts/daily-characters.json'); const dailyCharacterIds = readJsonFile('./scripts/daily-characters.json');
if (!dailyCharacterIdsRaw || dailyCharacterIdsRaw.length === 0) { if (!dailyCharacterIds || dailyCharacterIds.length === 0) {
throw new Error('No daily characters found in daily-characters.json'); console.error('No daily characters found in daily-characters.json');
process.exit(1);
} }
const dailyCharacterIds = dailyCharacterIdsRaw;
console.log(`\n=== Setting Daily Mode Characters ===\n`); console.log(`\n=== Setting Daily Mode Characters ===\n`);
console.log(`Found ${dailyCharacterIds.length} characters to set as daily\n`); console.log(`Found ${dailyCharacterIds.length} characters to set as daily\n`);
@@ -46,36 +45,16 @@ async function setDailyCharacters(): Promise<void> {
let successCount = 0; let successCount = 0;
let errorCount = 0; let errorCount = 0;
const existingCharacters = await db
.select({ id: character.id })
.from(character)
.where(inArray(character.id, dailyCharacterIds));
const existingIdSet = new Set(existingCharacters.map((c) => c.id));
const missingIds = dailyCharacterIds.filter((id) => !existingIdSet.has(id));
if (missingIds.length > 0) {
errorCount += missingIds.length;
console.error(`${missingIds.length} character ID(s) were not found in database:`);
for (const missingId of missingIds) {
console.error(` - ${missingId}`);
}
console.error('');
}
for (let i = 0; i < dailyCharacterIds.length; i++) { for (let i = 0; i < dailyCharacterIds.length; i++) {
const charId = dailyCharacterIds[i]; const charId = dailyCharacterIds[i];
if (!existingIdSet.has(charId)) {
continue;
}
try { try {
await db const result = await db
.update(character) .update(character)
.set({ isInDailyMode: true }) .set({ isInDailyMode: true })
.where(eq(character.id, charId)); .where(eq(character.id, charId));
successCount++; successCount++;
process.stdout.write(`\rUpdated: ${successCount}/${dailyCharacterIds.length}`);
} catch (error) { } catch (error) {
errorCount++; errorCount++;
console.error(`\n✗ Error updating character ${i + 1}:`); console.error(`\n✗ Error updating character ${i + 1}:`);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,71 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from '$lib/server/daily-character';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { language, t } from '$lib/i18n'; import { createEventDispatcher } from 'svelte';
let { export let characters: any[];
characters, export let selectedCharacters: any[];
selectedCharacters,
onSelect
}: {
characters: CharacterWithRelations[];
selectedCharacters: CharacterWithRelations[];
onSelect: (character: CharacterWithRelations) => void;
} = $props();
const state = $state({ const dispatch = createEventDispatcher();
searchInput: '',
highlightedIndex: 0,
dropdownContainer: null as HTMLDivElement | null,
searchContainer: null as HTMLDivElement | null
});
const isFrench = $derived($language === 'fr'); let searchInput = '';
let highlightedIndex = 0;
function parseEpithets(value: unknown): string[] { let dropdownContainer: HTMLDivElement;
if (Array.isArray(value)) { let searchContainer: HTMLDivElement;
return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
} catch {
if (value.length > 0) {
return [value];
}
}
}
return [];
}
function getDisplayName(character: CharacterWithRelations): string {
if (isFrench && typeof character.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character.name;
}
function getDisplayEpithets(character: CharacterWithRelations): string[] {
const frenchEpithets = parseEpithets(character.frEpithets);
if (isFrench && frenchEpithets.length > 0) {
return frenchEpithets;
}
return parseEpithets(character.epithets);
}
function normalizeSearchText(value: string): string {
return value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
onMount(() => { onMount(() => {
// Add click outside listener // Add click outside listener
@@ -76,54 +21,52 @@
}; };
}); });
const filteredCharacters = $derived.by(() => { $: filteredCharacters = characters.filter(char => {
const searchTerm = normalizeSearchText(state.searchInput); const searchTerm = searchInput.toLowerCase();
const nameMatches = char.name.toLowerCase().includes(searchTerm);
return characters.filter((char) => { let epithetsMatches = false;
const displayName = getDisplayName(char); if (char.epithets) {
const displayEpithets = getDisplayEpithets(char); try {
const nameMatches = normalizeSearchText(displayName).includes(searchTerm); const parsedEpithets = typeof char.epithets === 'string'
const epithetsMatches = displayEpithets.some((epithet) => ? JSON.parse(char.epithets)
normalizeSearchText(epithet).includes(searchTerm) : char.epithets;
);
return (nameMatches || epithetsMatches) && if (Array.isArray(parsedEpithets)) {
!selectedCharacters.some((selected) => selected.id === char.id); epithetsMatches = parsedEpithets.some((epithet: string) =>
}); epithet.toLowerCase().includes(searchTerm)
}); );
} else if (typeof parsedEpithets === 'string') {
// Reset highlighted index when filtered list changes. epithetsMatches = parsedEpithets.toLowerCase().includes(searchTerm);
$effect(() => { }
const nextFilteredCharacters = filteredCharacters; } catch {
if (!nextFilteredCharacters) { epithetsMatches = String(char.epithets).toLowerCase().includes(searchTerm);
return; }
}
state.highlightedIndex = 0;
});
// Scroll highlighted item into view.
$effect(() => {
const nextFilteredCharacters = filteredCharacters;
if (!state.dropdownContainer || state.highlightedIndex < 0) {
return;
} }
if (state.highlightedIndex >= nextFilteredCharacters.length) { return (nameMatches || epithetsMatches) &&
return; !selectedCharacters.some(selected => selected.id === char.id);
}
const highlightedButton = state.dropdownContainer.querySelector(
`button:nth-child(${state.highlightedIndex + 1})`
) as HTMLElement | null;
highlightedButton?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}); });
function selectCharacter(character: CharacterWithRelations) { // Reset highlighted index when filtered list changes
onSelect(character); $: if (filteredCharacters) {
state.searchInput = ''; highlightedIndex = 0;
state.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) {
dispatch('select', character);
searchInput = '';
highlightedIndex = 0;
} }
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
@@ -132,19 +75,16 @@
switch (event.key) { switch (event.key) {
case 'ArrowDown': case 'ArrowDown':
event.preventDefault(); event.preventDefault();
state.highlightedIndex = Math.min( highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
state.highlightedIndex + 1,
filteredCharacters.length - 1
);
break; break;
case 'ArrowUp': case 'ArrowUp':
event.preventDefault(); event.preventDefault();
state.highlightedIndex = Math.max(state.highlightedIndex - 1, 0); highlightedIndex = Math.max(highlightedIndex - 1, 0);
break; break;
case 'Enter': case 'Enter':
event.preventDefault(); event.preventDefault();
if (filteredCharacters[state.highlightedIndex]) { if (filteredCharacters[highlightedIndex]) {
selectCharacter(filteredCharacters[state.highlightedIndex]); selectCharacter(filteredCharacters[highlightedIndex]);
} }
break; break;
} }
@@ -152,44 +92,44 @@
function submitGuess() { function submitGuess() {
if (filteredCharacters.length === 0) return; if (filteredCharacters.length === 0) return;
const characterToSelect = filteredCharacters[state.highlightedIndex] ?? filteredCharacters[0]; const characterToSelect =
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
if (characterToSelect) { if (characterToSelect) {
selectCharacter(characterToSelect); selectCharacter(characterToSelect);
} }
} }
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
if (state.searchContainer && !state.searchContainer.contains(event.target as Node)) { if (searchContainer && !searchContainer.contains(event.target as Node)) {
state.searchInput = ''; searchInput = '';
} }
} }
</script> </script>
<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"> <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">{$t.game.components.searchInput.title}</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 bind:this={state.searchContainer} class="relative w-full"> <div bind:this={searchContainer} class="relative w-full">
<input <input
bind:value={state.searchInput} 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={$t.game.components.searchInput.placeholder} placeholder="Nom du personnage"
type="text" type="text"
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
{#if state.searchInput.length > 0 && filteredCharacters.length > 0} {#if searchInput.length > 0 && filteredCharacters.length > 0}
<div bind:this={state.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"> <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)} {#each filteredCharacters as character, index (character.id)}
<button <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 === state.highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}" 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" type="button"
onmouseenter={() => (state.highlightedIndex = index)} onmouseenter={() => highlightedIndex = index}
onclick={() => selectCharacter(character)} onclick={() => selectCharacter(character)}
> >
{#if character.pictureUrl} {#if character.pictureUrl}
<img <img
src={character.pictureUrl} src={character.pictureUrl}
alt={getDisplayName(character)} alt={character.name}
loading="lazy"
class="w-12 h-12 rounded-full object-cover border border-amber-200/30" class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
/> />
{:else} {:else}
@@ -198,11 +138,16 @@
</div> </div>
{/if} {/if}
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold text-amber-100">{getDisplayName(character)}</span> <span class="font-semibold text-amber-100">{character.name}</span>
{#if getDisplayEpithets(character).length > 0} {#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"> <span class="ml-2 text-xs text-slate-400">
{getDisplayEpithets(character).join(', ')} {parsedEpithets.join(', ')}
</span> </span>
{/if}
{/if} {/if}
</div> </div>
</button> </button>
@@ -213,10 +158,10 @@
<button <button
type="button" type="button"
onclick={submitGuess} onclick={submitGuess}
disabled={state.searchInput.length === 0 || filteredCharacters.length === 0} disabled={searchInput.length === 0 || 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" 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"
> >
{$t.game.components.searchInput.submit} Valider
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,73 +0,0 @@
<script lang="ts">
import { t } from '$lib/i18n';
type TriedCharacter = {
id: string;
name: string;
pictureUrl: string | null;
};
type FriendTodayResult = {
userId: string;
name: string;
image: string | null;
tryCount: number;
triedCharacters: TriedCharacter[];
};
export let friendsTodayResults: FriendTodayResult[] = [];
</script>
{#if friendsTodayResults.length > 0}
<section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
<div class="mt-4 space-y-2">
{#each friendsTodayResults as friendResult (friendResult.userId)}
<div class="rounded-lg border border-white/10 bg-slate-950/50 px-4 py-3">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
{#if friendResult.image}
<img
src={friendResult.image}
alt={friendResult.name}
class="h-8 w-8 rounded-full border border-white/20 object-cover"
/>
{:else}
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-300/20 text-xs font-semibold text-amber-100">
{friendResult.name?.charAt(0).toUpperCase() || 'U'}
</div>
{/if}
<p class="text-sm font-semibold text-slate-100">{friendResult.name}</p>
</div>
<p class="text-sm text-amber-300">
{friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
</p>
</div>
<div class="mt-3 border-t border-white/10 pt-2">
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.daily.friendsTriedCharacters}
</p>
{#if friendResult.triedCharacters && friendResult.triedCharacters.length > 0}
<div class="mt-2 flex flex-wrap gap-2">
{#each friendResult.triedCharacters as triedCharacter (triedCharacter.id)}
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
{#if triedCharacter.pictureUrl}
<img
src={triedCharacter.pictureUrl}
alt={triedCharacter.name}
class="h-4 w-4 rounded-full object-cover"
/>
{/if}
{triedCharacter.name}
</span>
{/each}
</div>
{:else}
<p class="mt-1 text-xs text-slate-500">{$t.game.daily.friendsNoTriedCharacters}</p>
{/if}
</div>
</div>
{/each}
</div>
</section>
{/if}

View File

@@ -1,493 +1,241 @@
<script lang="ts"> <script lang="ts">
import { formatBounty } from '$lib'; import { formatBounty } from '$lib';
import type { CharacterWithRelations } from '$lib/server/daily-character';
import { language, t } from '$lib/i18n';
export let selectedCharacters: CharacterWithRelations[]; export let selectedCharacters: any[];
export let dailyCharacter: CharacterWithRelations; export let dailyCharacter: any;
export let columnVisibility: { export let columnVisibility: any;
status?: boolean;
gender?: boolean;
affiliation?: boolean;
devilFruitType?: boolean;
haki?: boolean;
bounty?: boolean;
height?: boolean;
age?: boolean;
origin?: boolean;
arc?: boolean;
};
$: isFrench = $language === 'fr';
function getDisplayName(character: CharacterWithRelations): string {
if (isFrench && typeof character.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character.name;
}
function getWikiUrl(character: CharacterWithRelations): string {
if (isFrench && typeof character.frUrl === 'string' && character.frUrl.length > 0) {
return character.frUrl;
}
return character.url || '';
}
function getWikiBaseUrl(): string {
return isFrench ? 'https://onepiece.fandom.com/fr/wiki/' : 'https://onepiece.fandom.com/wiki/';
}
function getDisplayOrigin(character: CharacterWithRelations): string | null {
if (isFrench && typeof character.frOrigin === 'string' && character.frOrigin.length > 0) {
return character.frOrigin;
}
return character.origin;
}
function hasMatchingOrigin(characterEntry: CharacterWithRelations, dailyEntry: CharacterWithRelations): boolean {
return getDisplayOrigin(characterEntry) === getDisplayOrigin(dailyEntry);
}
function getDisplayArcName(character: CharacterWithRelations): string | null {
if (isFrench && typeof character.frArcName === 'string' && character.frArcName.length > 0) {
return character.frArcName;
}
return character.arcName;
}
function getDislayAffiliation(character: CharacterWithRelations): string | null {
if (isFrench && typeof character.frAffiliation === 'string' && character.frAffiliation.length > 0) {
return character.frAffiliation;
}
return character.affiliation;
}
function hasMatchingArc(characterEntry: CharacterWithRelations, dailyEntry: CharacterWithRelations): boolean {
return getDisplayArcName(characterEntry) === getDisplayArcName(dailyEntry);
}
</script> </script>
<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">
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"> <div class="flex flex-col gap-4">
<div class="flex flex-col items-center gap-4 text-center"> <div class="flex flex-col items-center gap-4 text-center">
<p class="text-xs font-semibold tracking-[0.28em] text-amber-100 uppercase">{$t.game.components.guessHistory.title}</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
</div> </div>
{#if selectedCharacters.length === 0} {#if selectedCharacters.length === 0}
<p class="text-center text-sm text-slate-200">{$t.game.components.guessHistory.empty}</p> <p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
{:else} {:else}
<div class="-mx-6 overflow-x-auto px-6 pb-2 sm:mx-0 sm:px-0"> <div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
<div class="mx-auto w-max min-w-max"> <div class="w-max min-w-max mx-auto">
<!-- Header --> <!-- Header -->
<div class="mb-2 flex gap-1 sm:gap-2"> <div class="flex gap-1 sm:gap-2 mb-2">
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.character}
</p>
</div> </div>
{#if columnVisibility.status !== false} {#if columnVisibility.status !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.status}
</p>
</div>
{/if} {/if}
{#if columnVisibility.gender !== false} {#if columnVisibility.gender !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.gender}
</p>
</div>
{/if} {/if}
{#if columnVisibility.affiliation !== false} {#if columnVisibility.affiliations !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.affiliations}
</p>
</div>
{/if} {/if}
{#if columnVisibility.devilFruitType !== false} {#if columnVisibility.devilFruitType !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.fruit}
</p>
</div>
{/if} {/if}
{#if columnVisibility.haki !== false} {#if columnVisibility.haki !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.haki}
</p>
</div>
{/if} {/if}
{#if columnVisibility.bounty !== false} {#if columnVisibility.bounty !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.bounty}
</p>
</div>
{/if} {/if}
{#if columnVisibility.height !== false} {#if columnVisibility.height !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.height}
</p>
</div>
{/if}
{#if columnVisibility.age !== false}
<div
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.age}
</p>
</div>
{/if} {/if}
{#if columnVisibility.origin !== false} {#if columnVisibility.origin !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.origin}
</p>
</div>
{/if} {/if}
{#if columnVisibility.arc !== false} {#if columnVisibility.arc !== false}
<div <div class="w-16 sm:w-20 md:w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 sm:p-2 text-center flex items-center justify-center">
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24" <p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
> </div>
<p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
>
{$t.game.components.guessHistory.arc}
</p>
</div>
{/if} {/if}
</div> </div>
<!-- Rows --> <!-- Rows -->
{#each selectedCharacters as character (character.id)} {#each selectedCharacters as character (character.id)}
<div class="mb-2 flex gap-1 sm:gap-2"> <div class="flex gap-1 sm:gap-2 mb-2">
<!-- Personnage --> <!-- Personnage -->
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
class="h-16 w-16 shrink-0 overflow-hidden rounded-lg border border-white/10 bg-slate-950/60 sm:h-20 sm:w-20 md:h-24 md:w-24"
>
{#if character.pictureUrl} {#if character.pictureUrl}
<a <a
href={getWikiBaseUrl() + getWikiUrl(character)} href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="block h-full w-full" class="block w-full h-full"
> >
<img <img
src={character.pictureUrl} src={character.pictureUrl}
alt={getDisplayName(character)} alt={character.name}
class="h-full w-full cursor-pointer object-cover transition-opacity hover:opacity-80" class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
/> />
</a> </a>
{:else} {:else}
<div <div class="w-full h-full bg-slate-800 flex items-center justify-center p-1 sm:p-2">
class="flex h-full w-full items-center justify-center bg-slate-800 p-1 sm:p-2" <span class="text-xs sm:text-sm md:text-xl text-center font-semibold line-clamp-3">{character.name}</span>
>
<span
class="line-clamp-3 text-center text-xs font-semibold sm:text-sm md:text-xl"
>{getDisplayName(character)}</span
>
</div> </div>
{/if} {/if}
</div> </div>
<!-- Vivant / Mort --> <!-- Vivant / Mort -->
{#if columnVisibility.status !== false} {#if columnVisibility.status !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.status === <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">
dailyCharacter.status {character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
? 'bg-emerald-600/90' </p>
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2" </div>
>
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
{character.status === 'Alive'
? $t.game.components.guessHistory.alive
: character.status === 'Dead'
? $t.game.components.guessHistory.dead
: character.status === 'Unknown'
? $t.game.components.guessHistory.unknown
: character.status === null
? '-'
: character.status || $t.game.components.guessHistory.unknown}
</p>
</div>
{/if} {/if}
<!-- Genre --> <!-- Genre -->
{#if columnVisibility.gender !== false} {#if columnVisibility.gender !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.gender === <p class="text-xs sm:text-sm md:text-base font-bold text-white text-center">
dailyCharacter.gender {character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
? 'bg-emerald-600/90' </p>
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2" </div>
>
<p class="text-center text-xs font-bold text-white sm:text-sm md:text-base">
{character.gender === 'Male'
? $t.game.components.guessHistory.male
: character.gender === 'Female'
? $t.game.components.guessHistory.female
: character.gender || $t.game.components.guessHistory.unknown}
</p>
</div>
{/if} {/if}
<!-- Affiliations --> <!-- Affiliations -->
{#if columnVisibility.affiliation !== false} {#if columnVisibility.affiliations !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {(() => {
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {getDislayAffiliation(character) === getDislayAffiliation(dailyCharacter) try {
? 'bg-emerald-600/90' const charAff = typeof character.affiliations === 'string'
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2" ? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
> : character.affiliations;
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"> const dailyAff = typeof dailyCharacter.affiliations === 'string'
{getDislayAffiliation(character) || $t.game.components.guessHistory.unknown} ? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
</p> : dailyCharacter.affiliations;
</div> 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-1 sm: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-[10px] sm:text-xs md:text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
{:else}
<p class="w-full text-[10px] sm:text-xs md:text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
{/if}
{:else}
<p class="text-xs sm:text-sm md:text-base font-bold text-slate-400 text-center">-</p>
{/if}
</div>
{/if} {/if}
<!-- Fruit --> <!-- Fruit -->
{#if columnVisibility.devilFruitType !== false} {#if columnVisibility.devilFruitType !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.devilFruitType === {#if character.devilFruitType}
dailyCharacter.devilFruitType <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">{character.devilFruitType}</p>
? 'bg-emerald-600/90' {:else}
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2" <p class="text-2xl sm:text-3xl md:text-5xl font-bold text-white text-center"></p>
> {/if}
{#if character.devilFruitType} </div>
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
{character.devilFruitType}
</p>
{:else}
<p class="text-center text-2xl font-bold text-white sm:text-3xl md:text-5xl">
</p>
{/if}
</div>
{/if} {/if}
<!-- Haki --> <!-- Haki -->
{#if columnVisibility.haki !== false} {#if columnVisibility.haki !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {(() => {
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {(() => { if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
if ( return 'bg-emerald-600/90';
character.hakiObservation === dailyCharacter.hakiObservation && } else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
character.hakiArmament === dailyCharacter.hakiArmament && (character.hakiArmament && dailyCharacter.hakiArmament) ||
character.hakiConqueror === dailyCharacter.hakiConqueror (character.hakiConqueror && dailyCharacter.hakiConqueror)) {
) { return 'bg-yellow-600/80';
return 'bg-emerald-600/90'; } else {
} else if ( return 'bg-red-900/60';
(character.hakiObservation && dailyCharacter.hakiObservation) || }
(character.hakiArmament && dailyCharacter.hakiArmament) || })()} p-1 sm:p-2 flex items-center justify-center">
(character.hakiConqueror && dailyCharacter.hakiConqueror) <p class="text-sm sm:text-lg md:text-2xl font-bold text-white text-center">
) { {#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
return 'bg-yellow-600/80'; {#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
} else { {#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
return 'bg-red-900/60'; {#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
} <span class="text-2xl sm:text-3xl md:text-5xl"></span>
})()} flex items-center justify-center p-1 sm:p-2" {/if}
> </p>
<p class="text-center text-sm font-bold text-white sm:text-lg md:text-2xl"> </div>
{#if character.hakiObservation}<span title={$t.game.components.guessHistory.obsHakiTitle}>👁️</span
>{/if}
{#if character.hakiArmament}<span title={$t.game.components.guessHistory.armHakiTitle}>🦾</span>{/if}
{#if character.hakiConqueror}<span title={$t.game.components.guessHistory.kingHakiTitle}>👑</span>{/if}
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
<span class="text-2xl sm:text-3xl md:text-5xl"></span>
{/if}
</p>
</div>
{/if} {/if}
<!-- Prime --> <!-- Prime -->
{#if columnVisibility.bounty !== false} {#if columnVisibility.bounty !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center relative overflow-hidden">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.bounty === {#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
dailyCharacter.bounty <div class="absolute w-full h-full opacity-30 pointer-events-none" style="
? 'bg-emerald-600/90'
: 'bg-red-900/60'} relative flex items-center justify-center overflow-hidden p-1 sm:p-2"
>
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
<div
class="pointer-events-none absolute h-full w-full opacity-30"
style="
background-color: rgb(203, 213, 225); background-color: rgb(203, 213, 225);
clip-path: {character.bounty > dailyCharacter.bounty clip-path: {character.bounty > dailyCharacter.bounty
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)' ? '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%)'}; : 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
" "></div>
></div> {/if}
{/if} {#if character.bounty != null}
{#if character.bounty != null} <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
<p {:else}
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm" <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">Inconnue</p>
> {/if}
{formatBounty(character.bounty)} ฿ </div>
</p>
{:else}
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{$t.game.components.guessHistory.unknown}
</p>
{/if}
</div>
{/if} {/if}
<!-- Taille --> <!-- Taille -->
{#if columnVisibility.height !== false} {#if columnVisibility.height !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center relative overflow-hidden">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.height === {#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
dailyCharacter.height <div class="absolute w-full h-full opacity-30 pointer-events-none" style="
? 'bg-emerald-600/90'
: 'bg-red-900/60'} relative flex items-center justify-center overflow-hidden p-1 sm:p-2"
>
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
<div
class="pointer-events-none absolute h-full w-full opacity-30"
style="
background-color: rgb(203, 213, 225); background-color: rgb(203, 213, 225);
clip-path: {character.height > dailyCharacter.height clip-path: {character.height > dailyCharacter.height
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)' ? '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%)'}; : 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
" "></div>
></div> {/if}
{/if} {#if character.height}
{#if character.height} <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{character.height} m
</p>
{:else} {:else}
<p <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">Inconnue</p>
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm" {/if}
> </div>
{$t.game.components.guessHistory.unknown}
</p>
{/if}
</div>
{/if}
<!-- Age -->
{#if columnVisibility.age !== false}
<div
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.age ===
dailyCharacter.age
? 'bg-emerald-600/90'
: 'bg-red-900/60'} relative flex items-center justify-center overflow-hidden p-1 sm:p-2"
>
{#if character.age != null && dailyCharacter.age != null && character.age !== dailyCharacter.age}
<div
class="pointer-events-none absolute h-full w-full opacity-30"
style="
background-color: rgb(203, 213, 225);
clip-path: {character.age > dailyCharacter.age
? '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.age != null}
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{character.age}
</p>
{:else}
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{$t.game.components.guessHistory.unknown}
</p>
{/if}
</div>
{/if} {/if}
<!-- Origine --> <!-- Origine -->
{#if columnVisibility.origin !== false} {#if columnVisibility.origin !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {hasMatchingOrigin(character, dailyCharacter) <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
? 'bg-emerald-600/90' </div>
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2"
>
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
{getDisplayOrigin(character) || $t.game.components.guessHistory.unknown}
</p>
</div>
{/if} {/if}
<!-- Arc --> <!-- Arc -->
{#if columnVisibility.arc !== false} {#if columnVisibility.arc !== false}
<div <div class="w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-1 sm:p-2 flex items-center justify-center relative overflow-hidden">
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {hasMatchingArc(character, dailyCharacter) {#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
? 'bg-emerald-600/90' <div class="absolute w-full h-full opacity-30 pointer-events-none" style="
: 'bg-red-900/60'} relative flex items-center justify-center overflow-hidden p-1 sm:p-2"
>
{#if !hasMatchingArc(character, dailyCharacter) && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
<div
class="pointer-events-none absolute h-full w-full opacity-30"
style="
background-color: rgb(203, 213, 225); background-color: rgb(203, 213, 225);
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)' ? '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%)'}; : 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
" "></div>
></div> {/if}
{/if} <p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
<p </div>
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{getDisplayArcName(character) || $t.game.components.guessHistory.unknown}
</p>
</div>
{/if} {/if}
</div> </div>
{/each} {/each}

View File

@@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; export let dailyCharacter: any;
import { language, t } from '$lib/i18n'; export let selectedCharacters: any[];
export let dailyCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[];
export let showOriginUnlock: boolean = false; export let showOriginUnlock: boolean = false;
export let showFruitUnlock: boolean = false; export let showFruitUnlock: boolean = false;
export let showAffiliationUnlock: boolean = false; export let showAffiliationUnlock: boolean = false;
@@ -16,15 +13,6 @@
$: isOriginAvailable = selectedCharacters.length >= 5; $: isOriginAvailable = selectedCharacters.length >= 5;
$: isFruitAvailable = selectedCharacters.length >= 10; $: isFruitAvailable = selectedCharacters.length >= 10;
$: isAffiliationAvailable = selectedCharacters.length >= 15; $: isAffiliationAvailable = selectedCharacters.length >= 15;
$: isFrench = $language === 'fr';
function getDisplayOrigin(character: CharacterWithRelations): string | null {
if (isFrench && typeof character.frOrigin === 'string' && character.frOrigin.length > 0) {
return character.frOrigin;
}
return character.origin;
}
</script> </script>
<svelte:head> <svelte:head>
@@ -54,13 +42,13 @@
disabled={!isOriginAvailable} disabled={!isOriginAvailable}
onclick={() => showHintOrigin = !showHintOrigin} onclick={() => showHintOrigin = !showHintOrigin}
> >
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.origin}</p> <p class="text-sm font-medium text-amber-100">Origine</p>
{#if showHintOrigin} {#if showHintOrigin}
<p class="mt-2 text-xs text-white font-semibold">{getDisplayOrigin(dailyCharacter) || $t.game.components.hints.unknown}</p> <p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || 'Inconnue'}</p>
{:else if Math.max(0, 5 - selectedCharacters.length) > 0} {:else if Math.max(0, 5 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} essais avant déblocage</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p> <p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if} {/if}
</button> </button>
<button <button
@@ -69,13 +57,13 @@
disabled={!isFruitAvailable} disabled={!isFruitAvailable}
onclick={() => showHintFruit = !showHintFruit} onclick={() => showHintFruit = !showHintFruit}
> >
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.devilFruit}</p> <p class="text-sm font-medium text-amber-100">Fruit du démon</p>
{#if showHintFruit} {#if showHintFruit}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || $t.game.components.hints.none}</p> <p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || 'Aucun'}</p>
{:else if Math.max(0, 10 - selectedCharacters.length) > 0} {:else if Math.max(0, 10 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} essais avant déblocage</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p> <p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if} {/if}
</button> </button>
<button <button
@@ -84,13 +72,16 @@
disabled={!isAffiliationAvailable} disabled={!isAffiliationAvailable}
onclick={() => showHintAffiliation = !showHintAffiliation} onclick={() => showHintAffiliation = !showHintAffiliation}
> >
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p> <p class="text-sm font-medium text-amber-100">Affiliation</p>
{#if showHintAffiliation} {#if showHintAffiliation}
<p class="mt-2 text-xs text-white font-semibold">{isFrench && dailyCharacter.frAffiliation ? dailyCharacter.frAffiliation : dailyCharacter.affiliation || $t.game.components.hints.unknown}</p> {@const affiliations = 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}
<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} {:else if Math.max(0, 15 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} essais avant déblocage</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p> <p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if} {/if}
</button> </button>
</div> </div>

View File

@@ -1,104 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { availableLanguages, language, setLanguage } from '$lib/i18n';
let isOpen = false;
let rootElement: HTMLDivElement | undefined;
const languageLabels: Record<string, string> = {
en: 'English',
fr: 'Francais'
};
const languageFlags: Record<string, string> = {
en: 'GB',
fr: 'FR'
};
function getLanguageLabel(lang: string): string {
return languageLabels[lang] || lang.toUpperCase();
}
function getFlagCode(lang: string): string {
return languageFlags[lang] || 'UN';
}
function toFlagEmoji(code: string): string {
const normalized = code.toUpperCase();
if (normalized.length !== 2) {
return 'UN';
}
const first = normalized.codePointAt(0);
const second = normalized.codePointAt(1);
if (!first || !second) {
return 'UN';
}
return String.fromCodePoint(127397 + first, 127397 + second);
}
function toggleMenu() {
isOpen = !isOpen;
}
function selectLanguage(lang: string) {
setLanguage(lang);
isOpen = false;
}
onMount(() => {
const onDocumentClick = (event: MouseEvent) => {
if (!rootElement) {
return;
}
if (!rootElement.contains(event.target as Node)) {
isOpen = false;
}
};
document.addEventListener('click', onDocumentClick);
return () => document.removeEventListener('click', onDocumentClick);
});
</script>
<div bind:this={rootElement} class="relative">
<button
type="button"
onclick={toggleMenu}
class="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-2 text-sm font-semibold text-slate-100 transition hover:border-amber-300/50 hover:bg-white/10"
aria-haspopup="true"
aria-expanded={isOpen}
aria-label="Change language"
>
<span class="text-base" aria-hidden="true">{toFlagEmoji(getFlagCode($language))}</span>
<span class="uppercase text-xs tracking-wider">{$language}</span>
<svg
class="h-3.5 w-3.5 transition-transform {isOpen ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if isOpen}
<div class="absolute right-0 top-full z-20 mt-2 w-44 rounded-xl border border-white/10 bg-slate-900/95 p-1 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
{#each availableLanguages as lang (lang)}
<button
type="button"
onclick={() => selectLanguage(lang)}
class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition {lang === $language ? 'bg-amber-300 text-slate-900' : 'text-slate-100 hover:bg-white/5'}"
>
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">{toFlagEmoji(getFlagCode(lang))}</span>
<span>{getLanguageLabel(lang)}</span>
</span>
<span class="text-xs uppercase tracking-wide opacity-70">{lang}</span>
</button>
{/each}
</div>
{/if}
</div>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { User } from 'better-auth/types'; import type { User } from 'better-auth/types';
import { resolve } from '$app/paths';
interface Props { interface Props {
user: (User & { isAdmin?: boolean }) | null; user: (User & { isAdmin?: boolean }) | null;
@@ -60,7 +59,7 @@
{user.name?.charAt(0).toUpperCase() || 'U'} {user.name?.charAt(0).toUpperCase() || 'U'}
</div> </div>
{/if} {/if}
<span class="max-w-37.5 truncate text-sm font-semibold text-slate-100"> <span class="max-w-[150px] truncate text-sm font-semibold text-slate-100">
{user.name || 'Utilisateur'} {user.name || 'Utilisateur'}
</span> </span>
<svg <svg
@@ -78,15 +77,15 @@
class="absolute right-0 top-full mt-2 w-48 rounded-xl border border-white/10 bg-slate-900/95 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur" class="absolute right-0 top-full mt-2 w-48 rounded-xl border border-white/10 bg-slate-900/95 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"
> >
<a <a
href={resolve("/profile")} href="/profile"
onclick={closeMenu} onclick={closeMenu}
class="block border-b border-white/5 px-4 py-3 text-sm font-semibold text-slate-100 transition hover:bg-white/5 hover:text-amber-100 first:rounded-t-xl" class="block border-b border-white/5 px-4 py-3 text-sm font-semibold text-slate-100 transition hover:bg-white/5 hover:text-amber-100 first:rounded-t-xl"
> >
Voir mon profil Voir mon profil
</a> </a>
{#if (user).isAdmin} {#if (user as any).isAdmin}
<a <a
href={resolve("/admin")} href="/admin"
onclick={closeMenu} onclick={closeMenu}
class="block border-b border-white/5 px-4 py-3 text-sm font-semibold text-amber-300 transition hover:bg-white/5 hover:text-amber-200" class="block border-b border-white/5 px-4 py-3 text-sm font-semibold text-amber-300 transition hover:bg-white/5 hover:text-amber-200"
> >
@@ -103,7 +102,7 @@
{/if} {/if}
{:else} {:else}
<a <a
href={resolve("/login")} href="/login"
class="rounded-full bg-amber-300 px-5 py-2.5 text-sm font-semibold text-slate-900 transition hover:bg-amber-200" class="rounded-full bg-amber-300 px-5 py-2.5 text-sm font-semibold text-slate-900 transition hover:bg-amber-200"
> >
Se connecter Se connecter

View File

@@ -1,88 +1,31 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; export let dailyCharacter: any;
import { language, t } from '$lib/i18n'; export let selectedCharacters: any[];
export let selectedCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[];
export let isGeckoMoriaWin: boolean = false; export let isGeckoMoriaWin: boolean = false;
$: isFrench = $language === 'fr';
function getDisplayName(character: CharacterWithRelations): string {
if (isFrench && typeof character.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character.name;
}
function getWikiUrl(character: CharacterWithRelations): string {
if (isFrench && typeof character.frUrl === 'string' && character.frUrl.length > 0) {
return character.frUrl;
}
return character.url || '';
}
function getWikiBaseUrl(): string {
return isFrench ? 'https://onepiece.fandom.com/fr/wiki/' : 'https://onepiece.fandom.com/wiki/';
}
const pickMessage = (messages: readonly string[]) => messages[Math.floor(Math.random() * messages.length)];
const getAttemptMessage = (attempts: number): string => {
if (attempts <= 0) return '';
const oneTryMessages = $t.game.components.winPanel.oneTryMessages;
const twoTryMessages = $t.game.components.winPanel.twoTryMessages;
const tenPlusMessages = $t.game.components.winPanel.tenPlusMessages;
const fivePlusMessages = $t.game.components.winPanel.fivePlusMessages;
const defaultMessages = $t.game.components.winPanel.defaultMessages;
if (attempts === 1) {
return pickMessage(oneTryMessages);
}
if (attempts === 2) {
return pickMessage(twoTryMessages);
}
if (attempts >= 10) {
return pickMessage(tenPlusMessages).replace('${attempts}', String(attempts));
}
if (attempts >= 5) {
return pickMessage(fivePlusMessages).replace('${attempts}', String(attempts));
}
return pickMessage(defaultMessages);
};
$: attempts = selectedCharacters.length;
$: attemptMessage = getAttemptMessage(attempts);
$: attemptWord = selectedCharacters.length > 1
? $t.game.components.winPanel.attemptPlural
: $t.game.components.winPanel.attemptSingular;
</script> </script>
{#if isGeckoMoriaWin} {#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="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-center">
<div class="text-3xl mb-2">🌑</div> <div class="text-3xl mb-2">🌑</div>
<h2 class="text-xl font-bold text-slate-300 mb-1">{$t.game.components.winPanel.moriaTitle}</h2> <h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
<p class="text-sm text-slate-400">{$t.game.components.winPanel.moriaPrefix} {selectedCharacters.length} {attemptWord} !</p> <p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<p class="text-xs text-slate-300 mt-1">{attemptMessage}</p>
<div class="mt-3"> <div class="mt-3">
{#if selectedCharacter.pictureUrl} {#if dailyCharacter.pictureUrl}
<a <a
href={getWikiBaseUrl() + getWikiUrl(selectedCharacter)} href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-block" class="inline-block"
> >
<img <img
src={selectedCharacter.pictureUrl} src={dailyCharacter.pictureUrl}
alt={getDisplayName(selectedCharacter)} 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" 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> </a>
{/if} {/if}
<p class="mt-2 text-lg font-bold text-slate-200">{getDisplayName(selectedCharacter)}</p> <p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -90,25 +33,24 @@
<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="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-center">
<div class="text-3xl mb-2">🎉</div> <div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">{$t.game.components.winPanel.winTitle}</h2> <h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
<p class="text-sm text-emerald-300">{$t.game.components.winPanel.winPrefix} {selectedCharacters.length} {attemptWord} !</p> <p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<p class="text-xs text-emerald-200 mt-1">{attemptMessage}</p>
<div class="mt-3"> <div class="mt-3">
{#if selectedCharacter.pictureUrl} {#if dailyCharacter.pictureUrl}
<a <a
href={getWikiBaseUrl() + getWikiUrl(selectedCharacter)} href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-block" class="inline-block"
> >
<img <img
src={selectedCharacter.pictureUrl} src={dailyCharacter.pictureUrl}
alt={getDisplayName(selectedCharacter)} 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" 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> </a>
{/if} {/if}
<p class="mt-2 text-lg font-bold text-white">{getDisplayName(selectedCharacter)}</p> <p class="mt-2 text-lg font-bold text-white">{dailyCharacter.name}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,57 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; export let yesterdayCharacter: any;
import { language, t } from '$lib/i18n';
export let yesterdayCharacter: CharacterWithRelations | null;
$: isFrench = $language === 'fr';
function parseEpithets(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
} catch {
if (value.length > 0) {
return [value];
}
}
}
return [];
}
function getDisplayName(character: CharacterWithRelations): string {
if (isFrench && typeof character.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character.name;
}
function getDisplayEpithets(character: CharacterWithRelations): string[] {
const frenchEpithets = parseEpithets(character.frEpithets);
if (isFrench && frenchEpithets.length > 0) {
return frenchEpithets;
}
return parseEpithets(character.epithets);
}
function getWikiUrl(character: CharacterWithRelations): string {
if (isFrench && typeof character.frUrl === 'string' && character.frUrl.length > 0) {
return character.frUrl;
}
return character.url || '';
}
</script> </script>
<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">
@@ -60,52 +8,43 @@
{#if yesterdayCharacter.pictureUrl} {#if yesterdayCharacter.pictureUrl}
<img <img
src={yesterdayCharacter.pictureUrl} src={yesterdayCharacter.pictureUrl}
alt={getDisplayName(yesterdayCharacter)} alt={yesterdayCharacter.name}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover" class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/> />
{:else} {: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"> <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">
{$t.game.components.yesterdayCharacter.photo} Photo
</div> </div>
{/if} {/if}
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.components.yesterdayCharacter.title}</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">{getDisplayName(yesterdayCharacter)}</p> <p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if getDisplayEpithets(yesterdayCharacter).length > 0} {#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400"> <p class="mt-1 text-sm text-slate-400">
{getDisplayEpithets(yesterdayCharacter).join(', ')} {typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
</p> </p>
{/if} {/if}
</div> </div>
{#if isFrench} <a
<a href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
href="https://onepiece.fandom.com/fr/wiki/{getWikiUrl(yesterdayCharacter)}" target="_blank"
target="_blank" rel="noopener noreferrer"
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"
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
{$t.game.components.yesterdayCharacter.openPage} </a>
</a>
{:else}
<a
href="https://onepiece.fandom.com/wiki/{getWikiUrl(yesterdayCharacter)}"
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"
>
{$t.game.components.yesterdayCharacter.openPage}
</a>
{/if}
</div> </div>
{:else} {: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">
{$t.game.components.yesterdayCharacter.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">{$t.game.components.yesterdayCharacter.title}</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">{$t.game.components.yesterdayCharacter.none}</p> <p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">{$t.game.components.yesterdayCharacter.noneAvailable}</p> <p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -1,233 +0,0 @@
{
"common": {
"language": "Language",
"selectLanguage": "Select Language",
"english": "English",
"french": "Français",
"german": "Deutsch",
"spanish": "Español"
},
"game": {
"home": {
"heroDescription": "Guess the character from pirate crews, marines, or the wider world. Every hint brings you closer to the treasure.",
"dailyTitle": "Daily Character",
"dailySubtitle": "A new mystery every 24 hours",
"dailyDescription": "Compare your guesses, unlock hints, and keep your streak alive.",
"dailyCta": "Start",
"infiniteTitle": "Infinite Mode",
"infiniteSubtitle": "Endless challenges",
"infiniteDescription": "Chain characters and chase your score. No limits, only fun.",
"infiniteCta": "Play",
"photoFallback": "Photo",
"yesterdayCharacter": "Yesterday's character",
"openPage": "Open page",
"noCharacter": "No character",
"noYesterdayCharacter": "No character from yesterday available"
},
"login": {
"titleSignUp": "Sign Up",
"titleSignIn": "Sign In",
"headerSignUp": "Create your account",
"headerSignIn": "Welcome, pirate",
"nameLabel": "Name",
"namePlaceholder": "Your name",
"usernameLabel": "Username",
"usernamePlaceholder": "e.g. luffy_gear5",
"identifierLabelSignUp": "Email",
"identifierLabelSignIn": "Email or username",
"identifierPlaceholderSignUp": "yourmail@email.com",
"identifierPlaceholderSignIn": "yourmail@email.com or luffy_gear5",
"passwordLabel": "Password",
"confirmPasswordLabel": "Confirm password",
"loading": "Loading...",
"submitSignUp": "Create an account",
"submitSignIn": "Log in",
"togglePromptSignUp": "Already have an account?",
"togglePromptSignIn": "Don't have an account?",
"toggleActionSignUp": "Log in",
"toggleActionSignIn": "Sign up",
"backHome": "Back to home"
},
"profile": {
"pageTitle": "My Profile",
"headerTitle": "My Profile",
"headerSubtitle": "Edit your profile information",
"tabProfile": "Profile",
"tabPassword": "Password",
"tabDaily": "Daily History",
"tabSessions": "Sessions",
"tabFriends": "Friends",
"avatarFallbackAlt": "Profile",
"email": "Email",
"displayName": "Display name",
"displayNamePlaceholder": "Your name",
"profileUpdateSuccess": "Profile updated successfully!",
"updating": "Updating...",
"saveChanges": "Save changes",
"friendsTitle": "Friends System",
"addFriendByUsername": "Add a friend by username",
"friendUsernamePlaceholder": "e.g. luffy_gear5",
"sending": "Sending...",
"send": "Send",
"incomingRequests": "Incoming requests",
"noIncomingRequests": "No incoming requests.",
"accept": "Accept",
"decline": "Decline",
"outgoingRequests": "Outgoing requests",
"noOutgoingRequests": "No outgoing requests.",
"cancel": "Cancel",
"myFriends": "My friends",
"noFriends": "You don't have any friends yet.",
"remove": "Remove",
"changePasswordTitle": "Change password",
"currentPassword": "Current password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"passwordChangeSuccess": "Password changed successfully!",
"changing": "Changing...",
"changePassword": "Change password",
"dailyHistoryTitle": "Daily history",
"noDailyHistory": "No history available",
"triedCharactersTitle": "Tried characters",
"noTriedCharacters": "No characters recorded",
"noImage": "N/A",
"trySingular": "try",
"tryPlural": "tries",
"activeSessionsTitle": "Active sessions",
"noActiveSessions": "No active session",
"unknownDevice": "Unknown device",
"unknown": "Unknown",
"ip": "IP",
"created": "Created",
"terminate": "Terminate",
"backHome": "Back to home"
},
"daily": {
"metaTitle": "OnePieceDle - Daily Mode",
"title": "Daily Character",
"winsPeopleSingular": "person",
"winsPeoplePlural": "people",
"winsVerbSingular": "has",
"winsVerbPlural": "have",
"winsSuffix": "found it today 🎉",
"reset": "Play again",
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
"friendsToday": "Your friends today",
"friendsTriedCharacters": "Tried characters",
"friendsNoTriedCharacters": "No characters recorded",
"friendTrySingular": "try",
"friendTryPlural": "tries"
},
"infinite": {
"metaTitle": "OnePieceDle - Infinite Mode",
"title": "Infinite Mode",
"score": "Score",
"resetScore": "Reset",
"description": "Guess characters endlessly. Each hint unlocks after a certain number of guesses. Good luck!",
"nextCharacter": "Play again",
"revealAnswer": "Reveal answer",
"loadingCharacter": "Loading character...",
"filtersTitle": "Character filters",
"clearFilters": "Reset",
"filterGender": "Gender",
"filterStatus": "Status",
"filterAbilities": "Abilities",
"filterInformation": "Information",
"filterArcs": "Arcs",
"male": "Male",
"female": "Female",
"alive": "Alive",
"dead": "Dead",
"unknown": "Unknown",
"hasHaki": "Has Haki",
"fruitAll": "Fruit (All)",
"withFruit": "With Fruit",
"withoutFruit": "Without Fruit",
"heightDefined": "Height defined",
"ageDefined": "Age defined",
"originDefined": "Origin defined",
"availableCharactersSingular": "character available",
"availableCharactersPlural": "characters available",
"columnsTitle": "Columns"
},
"components": {
"searchInput": {
"title": "Enter a guess",
"placeholder": "Character name",
"submit": "Submit"
},
"hints": {
"origin": "Origin",
"devilFruit": "Devil fruit",
"affiliation": "Affiliation",
"unknown": "Unknown",
"none": "None",
"beforeUnlock": "guesses before unlock",
"available": "Hint available!"
},
"guessHistory": {
"title": "History",
"empty": "No guesses yet.",
"character": "Character",
"status": "Status",
"gender": "Gender",
"affiliations": "Affiliations",
"fruit": "Fruit",
"haki": "Haki",
"bounty": "Bounty",
"height": "Height",
"age": "Age",
"origin": "Origin",
"arc": "Arc",
"alive": "Alive",
"dead": "Dead",
"unknown": "Unknown",
"male": "Male",
"female": "Female",
"obsHakiTitle": "Observation Haki",
"armHakiTitle": "Armament Haki",
"kingHakiTitle": "Conqueror's Haki"
},
"winPanel": {
"attemptSingular": "attempt",
"attemptPlural": "attempts",
"moriaTitle": "Moria controls you...",
"moriaPrefix": "You succumbed to the shadows in",
"winTitle": "Congratulations!",
"winPrefix": "You found the character in",
"oneTryMessages": [
"Cheater 👀",
"1 guess? Admit it, you already knew 😏",
"First try... suspicious 🤨"
],
"twoTryMessages": [
"Well played! ⚡",
"Two guesses, clean! 👏",
"You warmed up fast, nice 🔥"
],
"tenPlusMessages": [
"${attempts} guesses... even a transponder snail would be faster 📞",
"${attempts} attempts? The Grand Line is shorter than that 😵",
"${attempts} guesses: legendary performance... in the wrong direction 🫠"
],
"fivePlusMessages": [
"${attempts} guesses? Let's say it was for suspense 😅",
"That is a lot of guesses... but you made it 😬",
"You never give up, even after several guesses 😂"
],
"defaultMessages": [
"Not bad at all!",
"Nice try, good pace 👍",
"Things are going well, keep it up ✨"
]
},
"yesterdayCharacter": {
"photo": "Photo",
"title": "Yesterday's character",
"openPage": "Open page",
"none": "No character",
"noneAvailable": "No character from yesterday available"
}
}
}
}

View File

@@ -1,233 +0,0 @@
{
"common": {
"language": "Langue",
"selectLanguage": "Sélectionnez la Langue",
"english": "English",
"french": "Français",
"german": "Deutsch",
"spanish": "Español"
},
"game": {
"home": {
"heroDescription": "Devine le personnage de l'equipage, des marines ou du vaste monde. Chaque indice te rapproche du tresor.",
"dailyTitle": "Personnage du jour",
"dailySubtitle": "Nouveau mystere toutes les 24 heures",
"dailyDescription": "Compare tes essais, debloque des indices et garde ta serie.",
"dailyCta": "Commencer",
"infiniteTitle": "Mode Infini",
"infiniteSubtitle": "Des defis sans fin",
"infiniteDescription": "Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.",
"infiniteCta": "Jouer",
"photoFallback": "Photo",
"yesterdayCharacter": "Personnage d'hier",
"openPage": "Voir la page",
"noCharacter": "Aucun personnage",
"noYesterdayCharacter": "Aucun personnage d'hier disponible"
},
"login": {
"titleSignUp": "Inscription",
"titleSignIn": "Connexion",
"headerSignUp": "Creer votre compte",
"headerSignIn": "Bienvenue, pirate",
"nameLabel": "Nom",
"namePlaceholder": "Votre nom",
"usernameLabel": "Nom d'utilisateur",
"usernamePlaceholder": "ex: luffy_gear5",
"identifierLabelSignUp": "E-mail",
"identifierLabelSignIn": "E-mail ou nom d'utilisateur",
"identifierPlaceholderSignUp": "votremail@email.com",
"identifierPlaceholderSignIn": "votremail@email.com ou luffy_gear5",
"passwordLabel": "Mot de passe",
"confirmPasswordLabel": "Confirmer le mot de passe",
"loading": "Chargement...",
"submitSignUp": "Creer un compte",
"submitSignIn": "Se connecter",
"togglePromptSignUp": "Vous avez deja un compte ?",
"togglePromptSignIn": "Vous n'avez pas de compte ?",
"toggleActionSignUp": "Se connecter",
"toggleActionSignIn": "S'inscrire",
"backHome": "Retour a l'accueil"
},
"profile": {
"pageTitle": "Mon Profil",
"headerTitle": "Mon Profil",
"headerSubtitle": "Modifie les informations de ton profil",
"tabProfile": "Profil",
"tabPassword": "Mot de passe",
"tabDaily": "Historique Daily",
"tabSessions": "Sessions",
"tabFriends": "Amis",
"avatarFallbackAlt": "Profil",
"email": "Email",
"displayName": "Nom d'affichage",
"displayNamePlaceholder": "Ton nom",
"profileUpdateSuccess": "Profil mis a jour avec succes !",
"updating": "Mise a jour...",
"saveChanges": "Enregistrer les modifications",
"friendsTitle": "Systeme d'amis",
"addFriendByUsername": "Ajouter un ami par nom d'utilisateur",
"friendUsernamePlaceholder": "ex: luffy_gear5",
"sending": "Envoi...",
"send": "Envoyer",
"incomingRequests": "Demandes recues",
"noIncomingRequests": "Aucune demande recue.",
"accept": "Accepter",
"decline": "Refuser",
"outgoingRequests": "Demandes envoyees",
"noOutgoingRequests": "Aucune demande envoyee.",
"cancel": "Annuler",
"myFriends": "Mes amis",
"noFriends": "Tu n'as pas encore d'amis.",
"remove": "Supprimer",
"changePasswordTitle": "Changer le mot de passe",
"currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"passwordChangeSuccess": "Mot de passe change avec succes !",
"changing": "Changement en cours...",
"changePassword": "Changer le mot de passe",
"dailyHistoryTitle": "Historique des Daily",
"noDailyHistory": "Aucun historique disponible",
"triedCharactersTitle": "Personnages essayes",
"noTriedCharacters": "Aucun personnage enregistre",
"noImage": "N/A",
"trySingular": "tentative",
"tryPlural": "tentatives",
"activeSessionsTitle": "Sessions actives",
"noActiveSessions": "Aucune session active",
"unknownDevice": "Appareil inconnu",
"unknown": "Inconnue",
"ip": "IP",
"created": "Creee",
"terminate": "Terminer",
"backHome": "Retour a l'accueil"
},
"daily": {
"metaTitle": "OnePieceDle - Mode du jour",
"title": "Personnage du jour",
"winsPeopleSingular": "personne",
"winsPeoplePlural": "personnes",
"winsVerbSingular": "a",
"winsVerbPlural": "ont",
"winsSuffix": "trouve aujourd'hui 🎉",
"reset": "Recommencer",
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
"friendsToday": "Tes amis aujourd'hui",
"friendsTriedCharacters": "Personnages essayes",
"friendsNoTriedCharacters": "Aucun personnage enregistre",
"friendTrySingular": "coup",
"friendTryPlural": "coups"
},
"infinite": {
"metaTitle": "OnePieceDle - Mode Infini",
"title": "Mode Infini",
"score": "Score",
"resetScore": "Reinitialiser",
"description": "Devine des personnages a l'infini ! Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
"nextCharacter": "Recommencer",
"revealAnswer": "Reveler la reponse",
"loadingCharacter": "Chargement du personnage...",
"filtersTitle": "Filtres de personnages",
"clearFilters": "Reinitialiser",
"filterGender": "Genre",
"filterStatus": "Statut",
"filterAbilities": "Capacites",
"filterInformation": "Informations",
"filterArcs": "Arcs",
"male": "Homme",
"female": "Femme",
"alive": "Vivant",
"dead": "Mort",
"unknown": "Inconnu",
"hasHaki": "A du Haki",
"fruitAll": "Fruit (Tous)",
"withFruit": "Avec Fruit",
"withoutFruit": "Sans Fruit",
"heightDefined": "Taille definie",
"ageDefined": "Age defini",
"originDefined": "Origine definie",
"availableCharactersSingular": "personnage disponible",
"availableCharactersPlural": "personnages disponibles",
"columnsTitle": "Colonnes"
},
"components": {
"searchInput": {
"title": "Entrer une supposition",
"placeholder": "Nom du personnage",
"submit": "Valider"
},
"hints": {
"origin": "Origine",
"devilFruit": "Fruit du demon",
"affiliation": "Affiliation",
"unknown": "Inconnue",
"none": "Aucun",
"beforeUnlock": "essais avant deblocage",
"available": "Indice disponible !"
},
"guessHistory": {
"title": "Historique",
"empty": "Aucune tentative pour le moment.",
"character": "Personnage",
"status": "Statut",
"gender": "Genre",
"affiliations": "Affiliations",
"fruit": "Fruit",
"haki": "Haki",
"bounty": "Prime",
"height": "Taille",
"age": "Age",
"origin": "Origine",
"arc": "Arc",
"alive": "Vivant",
"dead": "Mort",
"unknown": "Inconnu",
"male": "Homme",
"female": "Femme",
"obsHakiTitle": "Haki de l'Observation",
"armHakiTitle": "Haki de l'Armement",
"kingHakiTitle": "Haki des Rois"
},
"winPanel": {
"attemptSingular": "tentative",
"attemptPlural": "tentatives",
"moriaTitle": "Moria vous controle...",
"moriaPrefix": "Vous avez succombe a l'ombre en",
"winTitle": "Felicitations !",
"winPrefix": "Vous avez trouve le personnage en",
"oneTryMessages": [
"Tricheur 👀",
"1 essai ? Avoue, tu avais la reponse 😏",
"Premier coup direct... suspect 🤨"
],
"twoTryMessages": [
"Bien joue ! ⚡",
"Deux essais, propre ! 👏",
"Tu chauffes vite, bien joue 🔥"
],
"tenPlusMessages": [
"${attempts} essais... meme un escargophone aurait trouve plus vite 📞",
"${attempts} tentatives ? Le Grand Line est moins long que ca 😵",
"${attempts} essais : performance legendaire... dans le mauvais sens 🫠"
],
"fivePlusMessages": [
"${attempts} essais ? On va dire que c'etait pour le suspense 😅",
"Ca en fait des essais... mais au moins tu y es arrive 😬",
"Tu ne laches rien, meme apres plusieurs essais 😂"
],
"defaultMessages": [
"Pas mal du tout !",
"Bien tente, bon rythme 👍",
"Ca se passe bien, continue comme ca ✨"
]
},
"yesterdayCharacter": {
"photo": "Photo",
"title": "Personnage d'hier",
"openPage": "Voir la page",
"none": "Aucun personnage",
"noneAvailable": "Aucun personnage d'hier disponible"
}
}
}
}

View File

@@ -1,51 +0,0 @@
import { writable, derived } from 'svelte/store';
import type { Writable, Readable } from 'svelte/store';
import en from './en.json';
import fr from './fr.json';
type Messages = typeof en;
const translations: Record<string, Messages> = { en, fr };
// Get initial language
function getInitialLanguage(): string {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('language');
if (stored && stored in translations) {
return stored;
}
const browserLang = navigator.language.split('-')[0];
if (browserLang in translations) {
return browserLang;
}
}
return 'en';
}
// Create writable store for the current language
export const language: Writable<string> = writable(getInitialLanguage());
// Create derived store for the current messages
export const t: Readable<Messages> = derived(language, ($language) => {
return translations[$language] || translations['en'];
});
export function setLanguage(lang: string) {
if (lang in translations) {
if (typeof window !== 'undefined') {
localStorage.setItem('language', lang);
}
language.set(lang);
}
}
export function getLanguage(): string {
let currentLang = 'en';
language.subscribe((lang) => {
currentLang = lang;
})();
return currentLang;
}
export const availableLanguages = Object.keys(translations);

View File

@@ -9,15 +9,6 @@ export const auth = betterAuth({
baseURL: env.ORIGIN, baseURL: env.ORIGIN,
secret: env.BETTER_AUTH_SECRET || 'secret', secret: env.BETTER_AUTH_SECRET || 'secret',
database: drizzleAdapter(db, { provider: 'sqlite' }), database: drizzleAdapter(db, { provider: 'sqlite' }),
user: {
additionalFields: {
username: {
type: 'string',
required: true,
unique: true
}
}
},
emailAndPassword: { enabled: true }, emailAndPassword: { enabled: true },
plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array
}); });

View File

@@ -1,6 +1,6 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { arc, character, characterHistory, devilFruit, type Character } from '$lib/server/db/schema'; import { arc, character, characterHistory, characterOverride, devilFruit } from '$lib/server/db/schema';
import { desc, eq, and } from 'drizzle-orm'; import { desc, eq, inArray, and } from 'drizzle-orm';
// Generate or get random seed for daily character selection // Generate or get random seed for daily character selection
const RANDOM_SEED = Math.random(); const RANDOM_SEED = Math.random();
@@ -8,11 +8,9 @@ const RANDOM_SEED = Math.random();
const characterWithRelationsSelect = { const characterWithRelationsSelect = {
id: character.id, id: character.id,
name: character.name, name: character.name,
frName: character.frName,
gender: character.gender, gender: character.gender,
age: character.age, age: character.age,
affiliation: character.affiliation, affiliations: character.affiliations,
frAffiliation: character.frAffiliation,
devilFruitId: character.devilFruitId, devilFruitId: character.devilFruitId,
devilFruitName: devilFruit.name, devilFruitName: devilFruit.name,
devilFruitType: devilFruit.type, devilFruitType: devilFruit.type,
@@ -22,26 +20,23 @@ const characterWithRelationsSelect = {
bounty: character.bounty, bounty: character.bounty,
height: character.height, height: character.height,
origin: character.origin, origin: character.origin,
frOrigin: character.frOrigin,
firstAppearance: character.firstAppearance, firstAppearance: character.firstAppearance,
pictureUrl: character.pictureUrl, pictureUrl: character.pictureUrl,
epithets: character.epithets, epithets: character.epithets,
frEpithets: character.frEpithets,
status: character.status, status: character.status,
url: character.url, url: character.url,
frUrl: character.frUrl,
arcId: character.arcId, arcId: character.arcId,
arcName: arc.name, arcName: arc.name
frArcName: arc.frName,
}; };
export type CharacterWithRelations = Character & { export type CharacterWithRelations = typeof character.$inferSelect & {
devilFruitName: string | null; devilFruitName: string | null;
devilFruitType: string | null; devilFruitType: string | null;
arcName: string | null; arcName: string | null;
frArcName: string | null;
}; };
type CharacterOverrideRow = typeof characterOverride.$inferSelect;
type RelationMaps = { type RelationMaps = {
arcNameById: Map<string, string | null>; arcNameById: Map<string, string | null>;
devilFruitById: Map<string, { name: string | null; type: string | null }>; devilFruitById: Map<string, { name: string | null; type: string | null }>;
@@ -51,6 +46,102 @@ function isNotNullish<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }
function mergeCharacterWithOverride(
baseCharacter: CharacterWithRelations,
overrideRow?: CharacterOverrideRow,
relationMaps?: RelationMaps
): CharacterWithRelations {
if (!overrideRow) {
return baseCharacter;
}
const mergedCharacter = { ...baseCharacter } as CharacterWithRelations;
for (const [key, value] of Object.entries(overrideRow)) {
if (key === 'characterId' || key === 'notes') {
continue;
}
if (isNotNullish(value)) {
(mergedCharacter as Record<string, unknown>)[key] = value;
}
}
if (relationMaps) {
if (mergedCharacter.arcId) {
mergedCharacter.arcName = relationMaps.arcNameById.get(mergedCharacter.arcId) ?? null;
} else {
mergedCharacter.arcName = null;
}
if (mergedCharacter.devilFruitId) {
const devilFruitData = relationMaps.devilFruitById.get(mergedCharacter.devilFruitId);
mergedCharacter.devilFruitName = devilFruitData?.name ?? null;
mergedCharacter.devilFruitType = devilFruitData?.type ?? null;
} else {
mergedCharacter.devilFruitName = null;
mergedCharacter.devilFruitType = null;
}
}
return mergedCharacter;
}
async function applyCharacterOverrides(
characters: CharacterWithRelations[]
): Promise<CharacterWithRelations[]> {
if (characters.length === 0) {
return characters;
}
const characterIds = characters.map((currentCharacter) => currentCharacter.id);
const overrideRows = await db
.select()
.from(characterOverride)
.where(inArray(characterOverride.characterId, characterIds));
if (overrideRows.length === 0) {
return characters;
}
const overrideByCharacterId = new Map<string, CharacterOverrideRow>(
overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow])
);
const shouldRefreshRelations = overrideRows.some(
(overrideRow) => isNotNullish(overrideRow.arcId) || isNotNullish(overrideRow.devilFruitId)
);
let relationMaps: RelationMaps | undefined;
if (shouldRefreshRelations) {
const [allArcs, allDevilFruits] = await Promise.all([
db.select({ id: arc.id, name: arc.name }).from(arc),
db
.select({ id: devilFruit.id, name: devilFruit.name, type: devilFruit.type })
.from(devilFruit)
]);
relationMaps = {
arcNameById: new Map(allArcs.map((currentArc) => [currentArc.id, currentArc.name])),
devilFruitById: new Map(
allDevilFruits.map((currentDevilFruit) => [
currentDevilFruit.id,
{ name: currentDevilFruit.name, type: currentDevilFruit.type }
])
)
};
}
return characters.map((currentCharacter) =>
mergeCharacterWithOverride(
currentCharacter,
overrideByCharacterId.get(currentCharacter.id),
relationMaps
)
);
}
export function getDateKey(date: Date): number { export function getDateKey(date: Date): number {
return normalizeDay(date).getTime(); return normalizeDay(date).getTime();
} }
@@ -70,22 +161,26 @@ function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): C
} }
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> { export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
return (await db const characters = (await db
.select(characterWithRelationsSelect) .select(characterWithRelationsSelect)
.from(character) .from(character)
.leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(arc, eq(character.arcId, arc.id))
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
.where(eq(character.isInDailyMode, true)) .where(eq(character.isInDailyMode, true))
.all()) as CharacterWithRelations[]; .all()) as CharacterWithRelations[];
return applyCharacterOverrides(characters);
} }
export async function getAllCharacters(): Promise<CharacterWithRelations[]> { export async function getAllCharacters(): Promise<CharacterWithRelations[]> {
return (await db const characters = (await db
.select(characterWithRelationsSelect) .select(characterWithRelationsSelect)
.from(character) .from(character)
.leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(arc, eq(character.arcId, arc.id))
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
.all()) as CharacterWithRelations[]; .all()) as CharacterWithRelations[];
return applyCharacterOverrides(characters);
} }
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> { export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
@@ -101,7 +196,8 @@ export async function getCharacterById(characterId: string): Promise<CharacterWi
return null; return null;
} }
return found as CharacterWithRelations const [overriddenCharacter] = await applyCharacterOverrides([found as CharacterWithRelations]);
return overriddenCharacter ?? null;
} }
export async function getOrCreateTodayCharacter( export async function getOrCreateTodayCharacter(

View File

@@ -4,7 +4,6 @@ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
name: text("name").notNull(), name: text("name").notNull(),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }) emailVerified: integer("email_verified", { mode: "boolean" })
.default(false) .default(false)

View File

@@ -1,12 +1,8 @@
import { integer, sqliteTable, text, real, unique } from 'drizzle-orm/sqlite-core'; import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core';
import { user } from './auth.schema'; import { user } from './auth.schema';
import type { InferSelectModel } from 'drizzle-orm';
// Define devil fruit types // Define devil fruit types
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown'; export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown';
export type Status = 'Alive' | 'Dead' | 'Unknown';
export type FriendshipStatus = 'pending' | 'accepted' | 'declined';
// Define the site config table schema // Define the site config table schema
export const config = sqliteTable('config', { export const config = sqliteTable('config', {
@@ -18,132 +14,109 @@ export const config = sqliteTable('config', {
export const arc = sqliteTable('arc', { export const arc = sqliteTable('arc', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
frName: text('fr_name'), startChapter: integer('startChapter').notNull(),
startChapter: integer('start_chapter').notNull(), endChapter: integer('endChapter'),
endChapter: integer('end_chapter'),
url: text('url') url: text('url')
}); });
export type Arc = InferSelectModel<typeof arc>;
// Define the devil fruit table schema // Define the devil fruit table schema
export const devilFruit = sqliteTable('devil_fruit', { 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') url: text('url')
}); });
export type DevilFruit = InferSelectModel<typeof devilFruit>;
// Define the character table schema // Define the character table schema
export const character = sqliteTable('character', { export const character = sqliteTable('character', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
frName: text('fr_name'),
gender: text('gender'), gender: text('gender'),
age: integer('age'), age: integer('age'),
affiliation: text('affiliation'), affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
frAffiliation: text('fr_affiliation'), devilFruitId: text('devilFruitId').references(() => devilFruit.id),
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id), hakiObservation: integer('hakiObservation', { mode: 'boolean' }).default(false),
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false), hakiArmament: integer('hakiArmament', { mode: 'boolean' }).default(false),
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false), hakiConqueror: integer('hakiConqueror', { mode: 'boolean' }).default(false),
hakiConqueror: integer('haki_conqueror', { mode: 'boolean' }).default(false),
bounty: integer('bounty').default(0), bounty: integer('bounty').default(0),
height: real('height'), height: real('height'),
origin: text('origin'), origin: text('origin'),
frOrigin: text('fr_origin'), firstAppearance: integer('firstAppearance').notNull(),
firstAppearance: integer('first_appearance').notNull(), pictureUrl: text('pictureUrl'),
pictureUrl: text('picture_url'),
epithets: text('epithets', { mode: 'json' }).$type<string[]>(), epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
frEpithets: text('fr_epithets', { mode: 'json' }).$type<string[]>(), status: text('status'),
status: text('status').$type<Status | null>(), arcId: text('arcId').references(() => arc.id),
arcId: text('arc_id').references(() => arc.id, { onDelete: 'set null' }),
url: text('url'), url: text('url'),
frUrl: text('fr_url'), isInDailyMode: integer('isInDailyMode', { mode: 'boolean' }).default(true)
isInDailyMode: integer('is_in_daily_mode', { mode: 'boolean' }).default(false)
}); });
export type Character = InferSelectModel<typeof character>; // Define the character override table schema
export const characterOverride = sqliteTable('characterOverride', {
// Define the character scrape validation table schema characterId: text('characterId').primaryKey().references(() => character.id),
export const characterScrapeValidation = sqliteTable('character_scrape_validation', { name: text('name'),
id: text('id').primaryKey(),
name: text('name').notNull(),
frName: text('fr_name'),
gender: text('gender'), gender: text('gender'),
age: integer('age'), age: integer('age'),
affiliation: text('affiliation'), affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
frAffiliation: text('fr_affiliation'), devilFruitId: text('devilFruitId').references(() => devilFruit.id),
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'set null' }), hakiObservation: integer('hakiObservation', { mode: 'boolean' }),
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false), hakiArmament: integer('hakiArmament', { mode: 'boolean' }),
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false), hakiConqueror: integer('hakiConqueror', { mode: 'boolean' }),
hakiConqueror: integer('haki_conqueror', { mode: 'boolean' }).default(false),
bounty: integer('bounty'), bounty: integer('bounty'),
height: real('height'), height: real('height'),
origin: text('origin'), origin: text('origin'),
frOrigin: text('fr_origin'), firstAppearance: integer('firstAppearance'),
firstAppearance: integer('first_appearance').notNull(), pictureUrl: text('pictureUrl'),
pictureUrl: text('picture_url'),
epithets: text('epithets', { mode: 'json' }).$type<string[]>(), epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
frEpithets: text('fr_epithets', { mode: 'json' }).$type<string[]>(), status: text('status'),
status: text('status').$type<Status | null>(), arcId: text('arcId').references(() => arc.id),
arcId: text('arc_id').references(() => arc.id, { onDelete: 'set null' }),
url: text('url'), url: text('url'),
frUrl: text('fr_url'), notes: text('notes')
isDeleted: integer('is_deleted', { mode: 'boolean' }).default(false),
}); });
export type CharacterScrapeValidation = InferSelectModel<typeof characterScrapeValidation>; // 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 character history table schema // Define the caracter history table schema
export const characterHistory = sqliteTable('character_history', { export const characterHistory = sqliteTable('characterHistory', {
id: text('id') id: text('id')
.primaryKey() .primaryKey()
.$defaultFn(() => crypto.randomUUID()), .$defaultFn(() => crypto.randomUUID()),
characterId: text('character_id').references(() => character.id, { onDelete: 'cascade' }), characterId: text('characterId').references(() => character.id),
date: integer('date').notNull().unique(), date: integer('date').notNull().unique(),
won: integer('won').notNull().default(0), won: integer('won').notNull().default(0),
createdAt: integer('created_at').notNull().$default(() => Date.now()), createdAt: integer('createdAt').notNull().$default(() => Date.now()),
updatedAt: integer('updated_at').notNull().$default(() => Date.now()), updatedAt: integer('updatedAt').notNull().$default(() => Date.now()),
}); });
export type CharacterHistory = InferSelectModel<typeof characterHistory>;
// Define the user character history table schema // Define the user character history table schema
export const userCharacterHistory = sqliteTable('user_character_history', { export const userCharacterHistory = sqliteTable('userCharacterHistory', {
id: text('id') id: text('id')
.primaryKey() .primaryKey()
.$defaultFn(() => crypto.randomUUID()), .$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }), userId: text('userId').references(() => user.id),
characterHistoryId: text('character_history_id').references(() => characterHistory.id, { onDelete: 'cascade' }), characterHistoryId: text('characterHistoryId').references(() => characterHistory.id),
tryCount: integer('try_count').notNull(), tryCount: integer('tryCount').notNull(),
triedCharacterIds: text('tried_character_ids', { mode: 'json' }).$type<string[]>(), createdAt: integer('createdAt').notNull().$default(() => Date.now())
createdAt: integer('created_at').notNull().$default(() => Date.now()) });
}, (table) => [
unique().on(table.userId, table.characterHistoryId)
]);
export type UserCharacterHistory = InferSelectModel<typeof userCharacterHistory>;
// Define the friendship table schema (friend requests + accepted friends)
export const friendship = sqliteTable('friendship', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
requesterId: text('requester_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
addresseeId: text('addressee_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
status: text('status').$type<FriendshipStatus>().notNull().default('pending'),
createdAt: integer('created_at').notNull().$default(() => Date.now()),
updatedAt: integer('updated_at').notNull().$default(() => Date.now()),
}, (table) => [
unique().on(table.requesterId, table.addresseeId)
]);
export type Friendship = InferSelectModel<typeof friendship>;
export * from './auth.schema'; export * from './auth.schema';

View File

@@ -1,14 +1,12 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import ProfileButton from '$lib/components/ProfileButton.svelte'; import ProfileButton from '$lib/components/ProfileButton.svelte';
import { resolve } from '$app/paths';
let { children, data } = $props(); let { children, data } = $props();
const navItems = [ const navItems = [
{ href: '/admin', label: 'Dashboard', icon: '📊' }, { href: '/admin', label: 'Dashboard', icon: '📊' },
{ href: '/admin/characters', label: 'Characters', icon: '🗣️' }, { href: '/admin/characters', label: 'Characters', icon: '🗣️' },
{ href: '/admin/character-changes', label: 'Changes', icon: '🔄' },
{ href: '/admin/devil-fruits', label: 'Devil Fruits', icon: '🍎' }, { href: '/admin/devil-fruits', label: 'Devil Fruits', icon: '🍎' },
{ href: '/admin/arcs', label: 'Arcs', icon: '📚' }, { href: '/admin/arcs', label: 'Arcs', icon: '📚' },
{ href: '/admin/users', label: 'Users', icon: '👥' }, { href: '/admin/users', label: 'Users', icon: '👥' },
@@ -30,9 +28,9 @@
<h2 class="text-lg font-black uppercase tracking-[0.15em] text-amber-50">Admin</h2> <h2 class="text-lg font-black uppercase tracking-[0.15em] text-amber-50">Admin</h2>
</div> </div>
<nav class="flex-1 space-y-2 px-3"> <nav class="flex-1 space-y-2 px-3">
{#each navItems as item (item.label)} {#each navItems as item}
<a <a
href={resolve(item.href)} href={item.href}
class={`flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium transition-colors ${ class={`flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium transition-colors ${
isActive(item.href, $page.url.pathname) isActive(item.href, $page.url.pathname)
? 'bg-amber-600 text-white' ? 'bg-amber-600 text-white'
@@ -46,7 +44,7 @@
</nav> </nav>
<div class="border-t border-white/5 p-3"> <div class="border-t border-white/5 p-3">
<a <a
href={resolve('/')} href="/"
class="flex items-center gap-2 rounded-lg px-4 py-3 text-sm font-medium text-gray-300 transition-colors hover:bg-slate-800 hover:text-white" class="flex items-center gap-2 rounded-lg px-4 py-3 text-sm font-medium text-gray-300 transition-colors hover:bg-slate-800 hover:text-white"
title="Return to site" title="Return to site"
> >

View File

@@ -2,16 +2,13 @@ import { db } from '$lib/server/db';
import { character, devilFruit, arc, user } from '$lib/server/db/schema'; import { character, devilFruit, arc, user } from '$lib/server/db/schema';
import { getOrCreateTodayCharacter, getTodayCharacterWinsCount } from '$lib/server/daily-character'; import { getOrCreateTodayCharacter, getTodayCharacterWinsCount } from '$lib/server/daily-character';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { count, eq } from 'drizzle-orm';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const [totalCharacters, totalDevilFruits, totalArcs, totalUsers, adminUsers, charactersInDaily] = await Promise.all([ const [characters, devilFruits, arcs, users] = await Promise.all([
db.select({ count: count() }).from(character), db.select().from(character),
db.select({ count: count() }).from(devilFruit), db.select().from(devilFruit),
db.select({ count: count() }).from(arc), db.select().from(arc),
db.select({ count: count() }).from(user), db.select().from(user)
db.select({ count: count() }).from(user).where(eq(user.isAdmin, true)),
db.select({ count: count() }).from(character).where(eq(character.isInDailyMode, true))
]); ]);
// Get today's daily character and count wins // Get today's daily character and count wins
@@ -24,12 +21,12 @@ export const load: PageServerLoad = async () => {
return { return {
stats: { stats: {
totalCharacters: totalCharacters[0].count, totalCharacters: characters.length,
charactersInDaily: charactersInDaily[0].count, charactersInDaily: characters.filter((c) => c.isInDailyMode).length,
totalDevilFruits: totalDevilFruits[0].count, totalDevilFruits: devilFruits.length,
totalArcs: totalArcs[0].count, totalArcs: arcs.length,
totalUsers: totalUsers[0].count, totalUsers: users.length,
adminUsers: adminUsers[0].count, adminUsers: users.filter((u) => u.isAdmin).length,
dailyCharacterWins dailyCharacterWins
} }
}; };

View File

@@ -1,272 +0,0 @@
import { db } from '$lib/server/db';
import { character, characterScrapeValidation } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
let isScrapeImportRunning = false;
const EXEC_OPTIONS = {
cwd: process.cwd(),
maxBuffer: 50 * 1024 * 1024
};
async function applyCharacterChangeFromScrapeValidation(characterId: string): Promise<boolean> {
const [scraped] = await db
.select()
.from(characterScrapeValidation)
.where(eq(characterScrapeValidation.id, characterId));
if (!scraped) {
return false;
}
if (scraped.isDeleted) {
await db.delete(character).where(eq(character.id, characterId));
return true;
}
await db
.insert(character)
.values({
id: scraped.id,
name: scraped.name,
frName: scraped.frName,
gender: scraped.gender,
age: scraped.age,
affiliation: scraped.affiliation,
frAffiliation: scraped.frAffiliation,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
hakiConqueror: scraped.hakiConqueror,
bounty: scraped.bounty,
height: scraped.height,
origin: scraped.origin,
frOrigin: scraped.frOrigin,
firstAppearance: scraped.firstAppearance,
pictureUrl: scraped.pictureUrl,
epithets: scraped.epithets,
frEpithets: scraped.frEpithets,
status: scraped.status,
arcId: scraped.arcId,
url: scraped.url,
frUrl: scraped.frUrl,
})
.onConflictDoUpdate({
target: character.id,
set: {
name: scraped.name,
frName: scraped.frName,
gender: scraped.gender,
age: scraped.age,
affiliation: scraped.affiliation,
frAffiliation: scraped.frAffiliation,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
hakiConqueror: scraped.hakiConqueror,
bounty: scraped.bounty,
height: scraped.height,
origin: scraped.origin,
frOrigin: scraped.frOrigin,
firstAppearance: scraped.firstAppearance,
pictureUrl: scraped.pictureUrl,
epithets: scraped.epithets,
frEpithets: scraped.frEpithets,
status: scraped.status,
arcId: scraped.arcId,
url: scraped.url,
frUrl: scraped.frUrl
}
});
return true;
}
export async function load() {
// Get all characters from both tables
const currentCharacters = await db.select().from(character);
const scrapedCharacters = await db.select().from(characterScrapeValidation);
// Create a map for quick lookup
const currentCharMap = new Map(currentCharacters.map(c => [c.id, c]));
// Compare and categorize changes
const changes: {
type: 'new' | 'modified' | 'deleted';
id: string;
scraped: (typeof scrapedCharacters)[0];
current?: (typeof currentCharacters)[0];
differences?: Record<string, { current: any; scraped: any }>;
}[] = [];
for (const scraped of scrapedCharacters) {
const current = currentCharMap.get(scraped.id);
if (scraped.isDeleted) {
if (current) {
changes.push({
type: 'deleted',
id: scraped.id,
scraped,
current
});
}
continue;
}
if (!current) {
// New character
changes.push({
type: 'new',
id: scraped.id,
scraped
});
} else {
// Check if different
const differences: Record<string, { current: any; scraped: any }> = {};
const fieldsToCompare = [
'name',
'frName',
'gender',
'age',
'affiliation',
'frAffiliation',
'devilFruitId',
'hakiObservation',
'hakiArmament',
'hakiConqueror',
'bounty',
'height',
'origin',
'frOrigin',
'firstAppearance',
'pictureUrl',
'epithets',
'frEpithets',
'status',
'arcId',
'url',
'frUrl'
];
for (const field of fieldsToCompare) {
const currentValue = current[field as keyof typeof current];
const scrapedValue = scraped[field as keyof typeof scraped];
// Deep comparison for JSON fields
if (JSON.stringify(currentValue) !== JSON.stringify(scrapedValue)) {
differences[field] = {
current: currentValue,
scraped: scrapedValue
};
}
}
if (Object.keys(differences).length > 0) {
changes.push({
type: 'modified',
id: scraped.id,
scraped,
current,
differences
});
}
}
}
const typeOrder: Record<'new' | 'modified' | 'deleted', number> = {
new: 0,
modified: 1,
deleted: 2
};
return {
changes: changes.sort((a, b) => {
if (a.type !== b.type) {
return typeOrder[a.type] - typeOrder[b.type];
}
return a.id.localeCompare(b.id);
})
};
}
export const actions = {
runScrapeImport: async () => {
if (isScrapeImportRunning) {
return {
success: false,
message: 'A scrape is already running. Please wait for it to finish.',
logs: ''
};
}
isScrapeImportRunning = true;
try {
const scrapeResult = await execAsync('npm run scrape', EXEC_OPTIONS);
const importResult = await execAsync('npm run db:import', EXEC_OPTIONS);
const logs = [
'=== npm run scrape ===',
scrapeResult.stdout || '',
scrapeResult.stderr ? `\n[stderr]\n${scrapeResult.stderr}` : '',
'\n=== npm run db:import ===',
importResult.stdout || '',
importResult.stderr ? `\n[stderr]\n${importResult.stderr}` : ''
]
.filter(Boolean)
.join('\n');
return {
success: true,
message: 'Scrape and import completed successfully',
logs
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to run scripts';
const stdout = typeof error === 'object' && error && 'stdout' in error ? String((error as any).stdout || '') : '';
const stderr = typeof error === 'object' && error && 'stderr' in error ? String((error as any).stderr || '') : '';
const logs = [stdout, stderr ? `\n[stderr]\n${stderr}` : ''].filter(Boolean).join('\n');
return {
success: false,
message,
logs
};
} finally {
isScrapeImportRunning = false;
}
},
acceptOne: async ({ request }) => {
const formData = await request.formData();
const characterId = formData.get('characterId');
if (!characterId || typeof characterId !== 'string') {
return { success: false, message: 'characterId is required' };
}
const applied = await applyCharacterChangeFromScrapeValidation(characterId);
return {
success: applied,
message: applied ? 'Character change applied successfully' : 'Character not found in scrape validation table'
};
},
acceptAll: async () => {
const scrapedCharacters = await db.select().from(characterScrapeValidation);
let appliedCount = 0;
for (const scraped of scrapedCharacters) {
const applied = await applyCharacterChangeFromScrapeValidation(scraped.id);
if (applied) {
appliedCount++;
}
}
return {
success: true,
appliedCount
};
}
};

View File

@@ -1,251 +0,0 @@
<script lang="ts">
type CharacterLike = {
name: string;
pictureUrl?: string | null;
url?: string | null;
status?: string | null;
gender?: string | null;
age?: number | null;
bounty?: number | null;
[key: string]: unknown;
};
type CharacterChange = {
type: 'new' | 'modified' | 'deleted';
id: string;
scraped: CharacterLike;
current?: CharacterLike;
differences?: Record<string, { current: unknown; scraped: unknown }>;
};
let { data, form } = $props();
const newCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'new'));
const modifiedCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'modified'));
const deletedCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'deleted'));
function formatValue(value: unknown): string {
if (value === null || value === undefined) {
return '—';
}
if (Array.isArray(value)) {
return value.join(', ');
}
if (typeof value === 'boolean') {
return value ? '✓' : '✗';
}
return String(value);
}
</script>
<svelte:head>
<title>Admin - Character Changes</title>
</svelte:head>
<div class="space-y-8">
<div>
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 mb-2">Character Changes</h1>
<p class="text-gray-400">Total changes: {newCharacters.length} new, {modifiedCharacters.length} modified, {deletedCharacters.length} deleted</p>
<form method="POST" action="?/runScrapeImport" class="mt-4">
<button
type="submit"
class="rounded-full border border-sky-300/40 bg-sky-500/20 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:bg-sky-500/30"
>
🔄 Lancer scrape + import
</button>
</form>
{#if form?.message}
<p class={`mt-3 text-sm ${form.success ? 'text-emerald-300' : 'text-rose-300'}`}>
{form.message}
</p>
{/if}
{#if form?.logs}
<pre class="mt-3 max-h-72 overflow-auto rounded-lg border border-white/10 bg-slate-900/70 p-3 text-xs text-slate-200 whitespace-pre-wrap">{form.logs}</pre>
{/if}
{#if newCharacters.length + modifiedCharacters.length + deletedCharacters.length > 0}
<form method="POST" action="?/acceptAll" class="mt-4">
<button
type="submit"
class="rounded-full border border-emerald-300/40 bg-emerald-500/20 px-4 py-2 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-500/30"
>
✅ Accepter tous les changements
</button>
</form>
{/if}
</div>
<!-- New Characters Section -->
{#if newCharacters.length > 0}
<section class="space-y-4">
<h2 class="text-xl font-bold text-emerald-400 uppercase tracking-[0.15em]">
🆕 New Characters ({newCharacters.length})
</h2>
<div class="grid gap-4">
{#each newCharacters as change (change.id)}
<div class="rounded-lg border border-emerald-500/30 bg-emerald-500/5 p-4 space-y-3">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
{#if change.scraped.pictureUrl}
<a href="https://onepiece.fandom.com/fr/wiki/{change.scraped.url}" target="_blank" rel="noopener noreferrer">
<img
src={change.scraped.pictureUrl ?? undefined}
alt={change.scraped.name}
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
/>
</a>
{/if}
<div>
<h3 class="font-bold text-emerald-300">{change.scraped.name}</h3>
<p class="text-sm text-gray-500">{change.id}</p>
</div>
</div>
<form method="POST" action="?/acceptOne">
<input type="hidden" name="characterId" value={change.id} />
<button
type="submit"
class="rounded-full border border-emerald-300/40 bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-100 transition hover:bg-emerald-500/30"
>
Accepter
</button>
</form>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-gray-400">Status:</span>
<span class="ml-2">{formatValue(change.scraped.status)}</span>
</div>
<div>
<span class="text-gray-400">Gender:</span>
<span class="ml-2">{formatValue(change.scraped.gender)}</span>
</div>
<div>
<span class="text-gray-400">Age:</span>
<span class="ml-2">{formatValue(change.scraped.age)}</span>
</div>
<div>
<span class="text-gray-400">Bounty:</span>
<span class="ml-2">{formatValue(change.scraped.bounty)}</span>
</div>
</div>
</div>
{/each}
</div>
</section>
{/if}
<!-- Modified Characters Section -->
{#if modifiedCharacters.length > 0}
<section class="space-y-4">
<h2 class="text-xl font-bold text-amber-400 uppercase tracking-[0.15em]">
✏️ Modified Characters ({modifiedCharacters.length})
</h2>
<div class="grid gap-6">
{#each modifiedCharacters as change (change.id)}
<div class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 space-y-4">
<div class="flex items-center justify-between gap-3 pb-4 border-b border-amber-500/20">
<div class="flex items-center gap-3">
{#if change.current?.pictureUrl}
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
<img
src={change.current?.pictureUrl ?? undefined}
alt={change.current?.name ?? change.scraped.name}
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
/>
</a>
{/if}
<div>
<h3 class="font-bold text-amber-300">{change.current?.name ?? change.scraped.name}</h3>
<p class="text-sm text-gray-500">{change.id}</p>
</div>
</div>
<form method="POST" action="?/acceptOne">
<input type="hidden" name="characterId" value={change.id} />
<button
type="submit"
class="rounded-full border border-amber-300/40 bg-amber-500/20 px-3 py-1 text-xs font-semibold text-amber-100 transition hover:bg-amber-500/30"
>
Accepter
</button>
</form>
</div>
{#if change.differences}
<div class="space-y-3">
{#each Object.entries(change.differences) as [field, diff] (field)}
<div class="bg-slate-900/50 rounded p-3 space-y-1">
<h4 class="text-sm font-semibold text-amber-100 uppercase tracking-widest">{field}</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="space-y-1">
<p class="text-gray-500">Current:</p>
<p class="font-mono text-gray-300">{formatValue(diff.current)}</p>
</div>
<div class="space-y-1">
<p class="text-gray-500">Scraped:</p>
<p class="font-mono text-emerald-300 font-semibold">{formatValue(diff.scraped)}</p>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
</section>
{/if}
<!-- Deleted Characters Section -->
{#if deletedCharacters.length > 0}
<section class="space-y-4">
<h2 class="text-xl font-bold text-rose-400 uppercase tracking-[0.15em]">
🗑️ Deleted Characters ({deletedCharacters.length})
</h2>
<div class="grid gap-4">
{#each deletedCharacters as change (change.id)}
<div class="rounded-lg border border-rose-500/30 bg-rose-500/5 p-4 space-y-3">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
{#if change.current?.pictureUrl}
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
<img
src={change.current?.pictureUrl ?? undefined}
alt={change.current?.name ?? change.scraped.name}
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
/>
</a>
{/if}
<div>
<h3 class="font-bold text-rose-300">{change.current?.name ?? change.scraped.name}</h3>
<p class="text-sm text-gray-500">{change.id}</p>
</div>
</div>
<form method="POST" action="?/acceptOne">
<input type="hidden" name="characterId" value={change.id} />
<button
type="submit"
class="rounded-full border border-rose-300/40 bg-rose-500/20 px-3 py-1 text-xs font-semibold text-rose-100 transition hover:bg-rose-500/30"
>
Supprimer
</button>
</form>
</div>
<p class="text-sm text-rose-200/80">This character is no longer present in the latest scrape and will be removed if accepted.</p>
</div>
{/each}
</div>
</section>
{/if}
{#if newCharacters.length === 0 && modifiedCharacters.length === 0 && deletedCharacters.length === 0}
<div class="rounded-lg border border-white/10 bg-white/5 p-8 text-center">
<p class="text-gray-400">Aucun changement détecté. Les tables character et characterScrapeValidation sont synchronisées.</p>
</div>
{/if}
</div>
<style>
:global(body) {
background-color: rgb(15, 23, 42);
color: rgb(203, 213, 225);
}
</style>

View File

@@ -1,32 +1,18 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { character, devilFruit, arc, type Status } from '$lib/server/db/schema'; import { character, devilFruit, arc, characterOverride } from '$lib/server/db/schema';
import { eq, sql } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
// Helper function to normalize data (parse JSON arrays)
const normalizeArray = (value: any): any => {
if (!value) return value;
if (Array.isArray(value)) return value;
if (typeof value === 'string' && value.includes('[')) {
try {
return JSON.parse(value);
} catch {
return value;
}
}
return value;
};
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
let [characters, devilFruits, arcs, statusesData, gendersData] = await Promise.all([ const [charactersData, devilFruits, arcs, overrides] = await Promise.all([
db db
.select({ .select({
id: character.id, id: character.id,
name: character.name, name: character.name,
gender: character.gender, gender: character.gender,
age: character.age, age: character.age,
affiliation: character.affiliation, affiliations: character.affiliations,
devilFruitId: character.devilFruitId, devilFruitId: character.devilFruitId,
hakiObservation: character.hakiObservation, hakiObservation: character.hakiObservation,
hakiArmament: character.hakiArmament, hakiArmament: character.hakiArmament,
@@ -36,7 +22,7 @@ export const load: PageServerLoad = async () => {
origin: character.origin, origin: character.origin,
firstAppearance: character.firstAppearance, firstAppearance: character.firstAppearance,
pictureUrl: character.pictureUrl, pictureUrl: character.pictureUrl,
epithets: normalizeArray(character.epithets), epithets: character.epithets,
status: character.status, status: character.status,
url: character.url, url: character.url,
arcId: character.arcId, arcId: character.arcId,
@@ -51,30 +37,112 @@ export const load: PageServerLoad = async () => {
.orderBy(character.name), .orderBy(character.name),
db.select().from(devilFruit).orderBy(devilFruit.name), db.select().from(devilFruit).orderBy(devilFruit.name),
db.select().from(arc).orderBy(arc.name), db.select().from(arc).orderBy(arc.name),
db.selectDistinct({ status: character.status }) db.select().from(characterOverride)
.from(character)
.where(sql`${character.status} IS NOT NULL AND ${character.status} != ''`),
db.selectDistinct({ gender: character.gender })
.from(character)
.where(sql`${character.gender} IS NOT NULL AND ${character.gender} != ''`)
]); ]);
// Create a map of overrides by characterId for easy lookup
const overridesMap = new Map(overrides.map((o) => [o.characterId, o]));
// Merge character data with overrides
const charactersWithOverrides = charactersData.map((char) => {
const override = overridesMap.get(char.id);
// Build displayValues by only applying non-null override fields
const displayValues = { ...char } as any;
if (override) {
Object.keys(override).forEach((key) => {
if (override[key as keyof typeof override] !== null && key !== 'characterId') {
displayValues[key as keyof typeof displayValues] = override[key as keyof typeof override];
}
});
}
return {
...char,
override,
displayValues
};
});
return { return {
characters, characters: charactersWithOverrides,
devilFruits, devilFruits,
arcs, arcs
availableStatuses: statusesData
.map(s => s.status)
.filter((s): s is Status => !!s)
.sort((a, b) => a.localeCompare(b)),
availableGenders: gendersData
.map(g => g.gender)
.filter((g): g is string => !!g)
.sort((a, b) => a.localeCompare(b))
}; };
}; };
export const actions: Actions = { export const actions: Actions = {
update: async ({ request, locals }) => {
if (!locals.user?.isAdmin) {
return fail(401, { error: 'Unauthorized' });
}
const formData = await request.formData();
const id = formData.get('id') as string;
if (!id) {
return fail(400, { error: 'Character ID is required' });
}
try {
const [originalCharacter] = await db
.select({
hakiObservation: character.hakiObservation,
hakiArmament: character.hakiArmament,
hakiConqueror: character.hakiConqueror
})
.from(character)
.where(eq(character.id, id))
.limit(1);
if (!originalCharacter) {
return fail(404, { error: 'Character not found' });
}
const updates: Record<string, any> = {};
formData.forEach((value, key) => {
if (key !== 'id') {
// Handle integers (age, bounty, height, devilFruitId, arcId)
if (key === 'age' || key === 'bounty' || key === 'height' || key === 'devilFruitId' || key === 'arcId') {
const strValue = value as string;
updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null;
}
// Handle checkboxes (haki fields) after parsing all form data
else if (key === 'hakiObservation' || key === 'hakiArmament' || key === 'hakiConqueror') {
return;
}
// Handle strings (name, gender, status, origin, affiliations, epithets, pictureUrl, url, firstAppearance)
else {
updates[key] = value || null;
}
}
});
const submittedHakiObservation = formData.has('hakiObservation');
const submittedHakiArmament = formData.has('hakiArmament');
const submittedHakiConqueror = formData.has('hakiConqueror');
updates.hakiObservation =
submittedHakiObservation === originalCharacter.hakiObservation ? null : submittedHakiObservation;
updates.hakiArmament =
submittedHakiArmament === originalCharacter.hakiArmament ? null : submittedHakiArmament;
updates.hakiConqueror =
submittedHakiConqueror === originalCharacter.hakiConqueror ? null : submittedHakiConqueror;
// Update or insert into characterOverride table
await db
.insert(characterOverride)
.values({ characterId: id, ...updates })
.onConflictDoUpdate({ target: characterOverride.characterId, set: updates });
return { success: true };
} catch (error) {
console.error('Character update error:', error);
return fail(500, { error: 'Failed to update character' });
}
},
delete: async ({ request, locals }) => { delete: async ({ request, locals }) => {
if (!locals.user?.isAdmin) { if (!locals.user?.isAdmin) {
return fail(401, { error: 'Unauthorized' }); return fail(401, { error: 'Unauthorized' });

View File

@@ -15,11 +15,13 @@
let filterGender = $state('all'); let filterGender = $state('all');
let filterArc = $state('all'); let filterArc = $state('all');
let filterHaki = $state<'all' | 'observation' | 'armament' | 'conqueror' | 'none'>('all'); let filterHaki = $state<'all' | 'observation' | 'armament' | 'conqueror' | 'none'>('all');
let selectedCharacterId = $state<string | null>(null);
let isEditModalOpen = $state(false); let isEditModalOpen = $state(false);
let isSaving = $state(false); let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null); let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let dailyModeToast = $state<{ type: 'success' | 'error'; text: string } | null>(null); let dailyModeToast = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let selectedChar = $state<any>(null); let selectedChar = $state<any>(null);
let showOriginalValue = $state<Record<string, boolean>>({});
const showDailyModeToast = (type: 'success' | 'error', text: string) => { const showDailyModeToast = (type: 'success' | 'error', text: string) => {
dailyModeToast = { type, text }; dailyModeToast = { type, text };
@@ -28,12 +30,10 @@
}, 3000); }, 3000);
}; };
const handleFileSelect = (event: Event) => { const getFandomUrl = (url: string | null | undefined) => {
const target = event.target as HTMLInputElement; if (!url) return null;
if (target.files && target.files.length > 0) { if (url.startsWith('http://') || url.startsWith('https://')) return url;
// Clear pictureUrl when a file is selected return `https://onepiece.fandom.com/fr/wiki/${url}`;
editForm.pictureUrl = '';
}
}; };
let editForm = $state<any>({ let editForm = $state<any>({
@@ -44,7 +44,7 @@
bounty: 0, bounty: 0,
height: 0, height: 0,
origin: '', origin: '',
affiliation: '', affiliations: '',
epithets: '', epithets: '',
pictureUrl: '', pictureUrl: '',
url: '', url: '',
@@ -57,28 +57,69 @@
status: '' status: ''
}); });
const availableStatuses = $derived.by(() => {
const statuses = new Set<string>();
for (const char of data.characters) {
const status = char.displayValues.status;
if (status && String(status).trim() !== '') {
statuses.add(String(status));
}
}
return Array.from(statuses).sort((a, b) => a.localeCompare(b));
});
const availableGenders = $derived.by(() => {
const genders = new Set<string>();
for (const char of data.characters) {
const gender = char.displayValues.gender;
if (gender && String(gender).trim() !== '') {
genders.add(String(gender));
}
}
return Array.from(genders).sort((a, b) => a.localeCompare(b));
});
const filteredCharacters = $derived.by(() => { const filteredCharacters = $derived.by(() => {
return data.characters.filter((char) => { return data.characters.filter((char) => {
const normalizedQuery = searchQuery.toLowerCase().trim(); const normalizedQuery = searchQuery.toLowerCase().trim();
let epithetsText = '';
if (char.displayValues.epithets) {
if (typeof char.displayValues.epithets === 'string') {
if (char.displayValues.epithets.includes('[')) {
try {
const parsed = JSON.parse(char.displayValues.epithets);
epithetsText = Array.isArray(parsed) ? parsed.join(' ') : String(parsed);
} catch {
epithetsText = char.displayValues.epithets;
}
} else {
epithetsText = char.displayValues.epithets;
}
} else if (Array.isArray(char.displayValues.epithets)) {
epithetsText = char.displayValues.epithets.join(' ');
}
}
const matchesSearch = const matchesSearch =
normalizedQuery === '' || normalizedQuery === '' ||
char.name.toLowerCase().includes(normalizedQuery); char.displayValues.name.toLowerCase().includes(normalizedQuery) ||
epithetsText.toLowerCase().includes(normalizedQuery);
const matchesDaily = const matchesDaily =
filterDaily === 'all' || filterDaily === 'all' ||
(filterDaily === 'daily' && char.isInDailyMode) || (filterDaily === 'daily' && char.displayValues.isInDailyMode) ||
(filterDaily === 'not-daily' && !char.isInDailyMode); (filterDaily === 'not-daily' && !char.displayValues.isInDailyMode);
const matchesStatus = filterStatus === 'all' || (char.status || '') === filterStatus; const matchesStatus = filterStatus === 'all' || (char.displayValues.status || '') === filterStatus;
const matchesGender = filterGender === 'all' || (char.gender || '') === filterGender; const matchesGender = filterGender === 'all' || (char.displayValues.gender || '') === filterGender;
const matchesArc = const matchesArc =
filterArc === 'all' || filterArc === 'all' ||
String(char.arcId ?? '') === filterArc; String(char.displayValues.arcId ?? '') === filterArc;
const matchesHaki = const matchesHaki =
filterHaki === 'all' || filterHaki === 'all' ||
(filterHaki === 'observation' && !!char.hakiObservation) || (filterHaki === 'observation' && !!char.displayValues.hakiObservation) ||
(filterHaki === 'armament' && !!char.hakiArmament) || (filterHaki === 'armament' && !!char.displayValues.hakiArmament) ||
(filterHaki === 'conqueror' && !!char.hakiConqueror) || (filterHaki === 'conqueror' && !!char.displayValues.hakiConqueror) ||
(filterHaki === 'none' && !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror); (filterHaki === 'none' && !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror);
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki; return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
}); });
@@ -89,6 +130,7 @@
}; };
const openEditModal = (char: any) => { const openEditModal = (char: any) => {
selectedCharacterId = char.id;
selectedChar = char; selectedChar = char;
const override = char.override || {}; const override = char.override || {};
@@ -101,23 +143,25 @@
bounty: override.bounty ?? null, bounty: override.bounty ?? null,
height: override.height ?? null, height: override.height ?? null,
origin: override.origin ?? '', origin: override.origin ?? '',
affiliation: override.affiliation ?? '', affiliations: override.affiliations ?? '',
epithets: override.epithets ?? '', epithets: override.epithets ?? '',
pictureUrl: override.pictureUrl ?? '', pictureUrl: override.pictureUrl ?? '',
url: override.url ?? '', url: override.url ?? '',
devilFruitId: override.devilFruitId !== null && override.devilFruitId !== undefined ? override.devilFruitId : (char.devilFruitId || ''), devilFruitId: override.devilFruitId !== null && override.devilFruitId !== undefined ? override.devilFruitId : '',
hakiObservation: override.hakiObservation ?? char.hakiObservation, hakiObservation: override.hakiObservation ?? char.hakiObservation,
hakiArmament: override.hakiArmament ?? char.hakiArmament, hakiArmament: override.hakiArmament ?? char.hakiArmament,
hakiConqueror: override.hakiConqueror ?? char.hakiConqueror, hakiConqueror: override.hakiConqueror ?? char.hakiConqueror,
firstAppearance: override.firstAppearance ?? '', firstAppearance: override.firstAppearance ?? '',
arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : (char.arcId || ''), arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : '',
status: override.status ?? '' status: override.status ?? ''
}; };
showOriginalValue = {};
isEditModalOpen = true; isEditModalOpen = true;
}; };
const closeModal = () => { const closeModal = () => {
isEditModalOpen = false; isEditModalOpen = false;
selectedCharacterId = null;
selectedChar = null; selectedChar = null;
editForm = { editForm = {
id: '', id: '',
@@ -127,7 +171,7 @@
bounty: 0, bounty: 0,
height: 0, height: 0,
origin: '', origin: '',
affiliation: '', affiliations: '',
epithets: '', epithets: '',
pictureUrl: '', pictureUrl: '',
url: '', url: '',
@@ -167,7 +211,6 @@
}, 3000); }, 3000);
} }
} catch (error) { } catch (error) {
console.error('Error deleting character:', error);
saveMessage = { saveMessage = {
type: 'error', type: 'error',
text: 'Error deleting character' text: 'Error deleting character'
@@ -210,7 +253,7 @@
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600" class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
> >
<option value="all">All Statuses</option> <option value="all">All Statuses</option>
{#each data.availableStatuses as status (status)} {#each availableStatuses as status}
<option value={status}>{status}</option> <option value={status}>{status}</option>
{/each} {/each}
</select> </select>
@@ -219,7 +262,7 @@
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600" class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
> >
<option value="all">All Genders</option> <option value="all">All Genders</option>
{#each data.availableGenders as gender (gender)} {#each availableGenders as gender}
<option value={gender}>{gender}</option> <option value={gender}>{gender}</option>
{/each} {/each}
</select> </select>
@@ -228,8 +271,8 @@
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600" class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
> >
<option value="all">All Arcs</option> <option value="all">All Arcs</option>
{#each data.arcs as arc (arc.id)} {#each data.arcs as arc}
<option value={arc.id}>{arc.name}</option> <option value={String(arc.id)}>{arc.name}</option>
{/each} {/each}
</select> </select>
<select <select
@@ -273,115 +316,120 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each filteredCharacters as char (char.id)} {#each filteredCharacters as char}
<tr class="border-b border-white/5 hover:bg-slate-800/50"> <tr class="border-b border-white/5 hover:bg-slate-800/50">
<!-- Character --> <!-- Character -->
<td class="px-4 py-4 text-sm text-white w-64 max-w-64"> <td class="px-4 py-4 text-sm text-white w-64 max-w-64 {isFieldOverridden(char, 'name') || isFieldOverridden(char, 'pictureUrl') ? 'bg-amber-500/10' : ''}">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
{#if char.url} {#if getFandomUrl(char.displayValues.url)}
<a <a
href={"https://onepiece.fandom.com/wiki/" + char.url} href={getFandomUrl(char.displayValues.url)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="shrink-0 transition-opacity hover:opacity-80" class="flex-shrink-0 transition-opacity hover:opacity-80"
> >
{#if char.pictureUrl} {#if char.displayValues.pictureUrl}
<img <img
src={char.pictureUrl} src={char.displayValues.pictureUrl}
alt={char.name} alt={char.displayValues.name}
loading="lazy"
class="h-10 w-10 rounded-full object-cover" class="h-10 w-10 rounded-full object-cover"
/> />
{:else} {:else}
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-700 text-gray-400"> <div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.name?.charAt(0).toUpperCase() || '?'} {char.displayValues.name?.charAt(0).toUpperCase() || '?'}
</div> </div>
{/if} {/if}
</a> </a>
{:else} {:else}
{#if char.pictureUrl} {#if char.displayValues.pictureUrl}
<img <img
src={char.pictureUrl} src={char.displayValues.pictureUrl}
alt={char.name} alt={char.displayValues.name}
loading="lazy" class="h-10 w-10 flex-shrink-0 rounded-full object-cover"
class="h-10 w-10 shrink-0 rounded-full object-cover"
/> />
{:else} {:else}
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400"> <div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.name?.charAt(0).toUpperCase() || '?'} {char.displayValues.name?.charAt(0).toUpperCase() || '?'}
</div> </div>
{/if} {/if}
{/if} {/if}
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
{#if char.url} {#if getFandomUrl(char.displayValues.url)}
<a <a
href="https://onepiece.fandom.com/wiki/{char.url}" href={getFandomUrl(char.displayValues.url)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="font-medium truncate text-white hover:text-amber-200 hover:underline" class="font-medium truncate text-white hover:text-amber-200 hover:underline"
> >
{char.name} {char.displayValues.name}
</a> </a>
{:else} {:else}
<span class="font-medium truncate">{char.name}</span> <span class="font-medium truncate">{char.displayValues.name}</span>
{/if} {/if}
{#if char.epithets} {#if char.displayValues.epithets}
<span class="text-xs text-gray-500 truncate"> <span class="text-xs text-gray-500 truncate">
{Array.isArray(char.epithets) {typeof char.displayValues.epithets === 'string'
? char.epithets.join(', ') ? (char.displayValues.epithets.includes('[') ? JSON.parse(char.displayValues.epithets).join(', ') : char.displayValues.epithets)
: char.epithets} : char.displayValues.epithets.join(', ')}
</span> </span>
{/if} {/if}
</div> </div>
</div> </div>
</td> </td>
<!-- Status --> <!-- Status -->
<td class="px-4 py-4 text-sm text-gray-400">{char.status || '-'}</td> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'status') ? 'bg-amber-500/10' : ''}">{char.displayValues.status || '-'}</td>
<!-- Gender --> <!-- Gender -->
<td class="px-4 py-4 text-sm text-gray-400">{char.gender || '-'}</td> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'gender') ? 'bg-amber-500/10' : ''}">{char.displayValues.gender || '-'}</td>
<!-- Affiliations --> <!-- Affiliations -->
<td class="px-4 py-4 text-sm text-gray-400"> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'affiliations') ? 'bg-amber-500/10' : ''}">
{#if char.affiliation} {#if char.displayValues.affiliations}
{char.affiliation} {@const parsedAffiliations = typeof char.displayValues.affiliations === 'string'
? (char.displayValues.affiliations.includes('[') ? JSON.parse(char.displayValues.affiliations) : char.displayValues.affiliations.split(',').map((a: string) => a.trim()))
: char.displayValues.affiliations}
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
<span class="inline-block" title={parsedAffiliations.join(', ')}>{parsedAffiliations[0]}</span>
{:else}
{parsedAffiliations}
{/if}
{:else} {:else}
- -
{/if} {/if}
</td> </td>
<!-- Fruit --> <!-- Fruit -->
<td class="px-4 py-4 text-sm text-gray-400">{char.devilFruitName || '-'}</td> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'devilFruitId') ? 'bg-amber-500/10' : ''}">{char.displayValues.devilFruitName || '-'}</td>
<!-- Haki --> <!-- Haki -->
<td class="px-4 py-4 text-sm"> <td class="px-4 py-4 text-sm {isFieldOverridden(char, 'hakiObservation') || isFieldOverridden(char, 'hakiArmament') || isFieldOverridden(char, 'hakiConqueror') ? 'bg-amber-500/10' : ''}">
<div class="flex gap-1"> <div class="flex gap-1">
{#if char.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if} {#if char.displayValues.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if char.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if} {#if char.displayValues.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if char.hakiConqueror}<span title="Haki des Rois">👑</span>{/if} {#if char.displayValues.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror} {#if !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror}
<span class="text-gray-400">-</span> <span class="text-gray-400">-</span>
{/if} {/if}
</div> </div>
</td> </td>
<!-- Bounty --> <!-- Bounty -->
<td class="px-4 py-4 text-sm text-gray-400"> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'bounty') ? 'bg-amber-500/10' : ''}">
{#if char.bounty != null} {#if char.displayValues.bounty != null}
{formatBounty(char.bounty)} ฿ {formatBounty(char.displayValues.bounty)} ฿
{:else} {:else}
- -
{/if} {/if}
</td> </td>
<!-- Height --> <!-- Height -->
<td class="px-4 py-4 text-sm text-gray-400"> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'height') ? 'bg-amber-500/10' : ''}">
{#if char.height} {#if char.displayValues.height}
{char.height} m {char.displayValues.height} m
{:else} {:else}
- -
{/if} {/if}
</td> </td>
<!-- Origin --> <!-- Origin -->
<td class="px-4 py-4 text-sm text-gray-400">{char.origin || '-'}</td> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'origin') ? 'bg-amber-500/10' : ''}">{char.displayValues.origin || '-'}</td>
<!-- Arc --> <!-- Arc -->
<td class="px-4 py-4 text-sm text-gray-400">{char.arcName || '-'}</td> <td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'arcId') || isFieldOverridden(char, 'arcName') ? 'bg-amber-500/10' : ''}">{char.displayValues.arcName || '-'}</td>
<!-- Daily Mode --> <!-- Daily Mode -->
<td class="px-4 py-4 text-sm"> <td class="px-4 py-4 text-sm {isFieldOverridden(char, 'isInDailyMode') ? 'bg-amber-500/10' : ''}">
<form <form
method="POST" method="POST"
action="?/toggleDailyMode" action="?/toggleDailyMode"
@@ -399,11 +447,11 @@
}} }}
> >
<input type="hidden" name="id" value={char.id} /> <input type="hidden" name="id" value={char.id} />
<input type="hidden" name="isInDailyMode" value={(!char.isInDailyMode).toString()} /> <input type="hidden" name="isInDailyMode" value={(!char.displayValues.isInDailyMode).toString()} />
<label class="flex items-center justify-center cursor-pointer"> <label class="flex items-center justify-center cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={char.isInDailyMode} checked={char.displayValues.isInDailyMode}
onchange={(e) => { onchange={(e) => {
const form = e.currentTarget.closest('form'); const form = e.currentTarget.closest('form');
if (form) form.requestSubmit(); if (form) form.requestSubmit();
@@ -446,7 +494,7 @@
{/if} {/if}
{#if dailyModeToast} {#if dailyModeToast}
<div class="fixed right-6 top-6 z-60"> <div class="fixed right-6 top-6 z-[60]">
<div <div
class={`rounded-lg border px-4 py-3 text-sm font-medium shadow-lg backdrop-blur ${ class={`rounded-lg border px-4 py-3 text-sm font-medium shadow-lg backdrop-blur ${
dailyModeToast.type === 'success' dailyModeToast.type === 'success'
@@ -468,7 +516,6 @@
class="mt-6 space-y-4" class="mt-6 space-y-4"
method="POST" method="POST"
action="?/update" action="?/update"
enctype="multipart/form-data"
use:enhance={() => { use:enhance={() => {
isSaving = true; isSaving = true;
return async ({ result }) => { return async ({ result }) => {
@@ -613,7 +660,7 @@
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white" class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
> >
<option value="">None</option> <option value="">None</option>
{#each data.arcs as arc (arc.id)} {#each data.arcs as arc}
<option value={arc.id}>{arc.name}</option> <option value={arc.id}>{arc.name}</option>
{/each} {/each}
</select> </select>
@@ -636,7 +683,7 @@
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white" class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
> >
<option value="">None</option> <option value="">None</option>
{#each data.devilFruits as fruit (fruit.id)} {#each data.devilFruits as fruit}
<option value={fruit.id}>{fruit.name}</option> <option value={fruit.id}>{fruit.name}</option>
{/each} {/each}
</select> </select>
@@ -729,24 +776,10 @@
name="pictureUrl" name="pictureUrl"
bind:value={editForm.pictureUrl} bind:value={editForm.pictureUrl}
placeholder={selectedChar?.pictureUrl || ''} placeholder={selectedChar?.pictureUrl || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40" class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/> />
</div> </div>
<div>
<label for="char-picture-file" class="block text-sm font-medium text-gray-300 mb-2">Or Upload Picture</label>
<input
type="file"
id="char-picture-file"
name="pictureFile"
accept="image/*"
onchange={handleFileSelect}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-amber-600 file:text-white hover:file:bg-amber-700"
/>
<p class="mt-1 text-xs text-gray-400">File will be saved as {selectedChar?.id}.jpg/png/etc</p>
</div>
<div> <div>
<label for="char-url" class="block text-sm font-medium text-gray-300 mb-2">Fandom URL</label> <label for="char-url" class="block text-sm font-medium text-gray-300 mb-2">Fandom URL</label>
<input <input

View File

@@ -13,10 +13,7 @@
let { data }: Props = $props(); let { data }: Props = $props();
let configItems = $derived(data.config.map((item) => ({ let configItems = $state<ConfigItem[]>([]);
key: item.key,
value: item.value ?? ''
})));
let newKey = $state(''); let newKey = $state('');
let newValue = $state(''); let newValue = $state('');
let editingKey = $state<string | null>(null); let editingKey = $state<string | null>(null);
@@ -24,7 +21,12 @@
let isSaving = $state(false); let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null); let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
; $effect(() => {
configItems = data.config.map((item) => ({
key: item.key,
value: item.value ?? ''
}));
});
const startEdit = (item: ConfigItem) => { const startEdit = (item: ConfigItem) => {
editingKey = item.key; editingKey = item.key;
@@ -68,7 +70,6 @@
saveMessage = { type: 'error', text: 'Failed to add config' }; saveMessage = { type: 'error', text: 'Failed to add config' };
} }
} catch (error) { } catch (error) {
console.error('Error adding config:', error);
saveMessage = { type: 'error', text: 'Error adding config' }; saveMessage = { type: 'error', text: 'Error adding config' };
} finally { } finally {
isSaving = false; isSaving = false;
@@ -98,7 +99,6 @@
saveMessage = { type: 'error', text: 'Failed to delete config' }; saveMessage = { type: 'error', text: 'Failed to delete config' };
} }
} catch (error) { } catch (error) {
console.error('Error deleting config:', error);
saveMessage = { type: 'error', text: 'Error deleting config' }; saveMessage = { type: 'error', text: 'Error deleting config' };
} finally { } finally {
isSaving = false; isSaving = false;
@@ -155,7 +155,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each configItems as item (item.key)} {#each configItems as item}
{#if editingKey === item.key} {#if editingKey === item.key}
<tr class="border-b border-white/5 bg-slate-800/50"> <tr class="border-b border-white/5 bg-slate-800/50">
<td class="px-6 py-4 text-sm text-white">{item.key}</td> <td class="px-6 py-4 text-sm text-white">{item.key}</td>

View File

@@ -11,6 +11,7 @@
let searchQuery = $state(''); let searchQuery = $state('');
let filterType = $state<'all' | 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown'>('all'); let filterType = $state<'all' | 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown'>('all');
let isEditModalOpen = $state(false); let isEditModalOpen = $state(false);
let selectedFruitId = $state<string | null>(null);
let isSaving = $state(false); let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null); let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
@@ -32,12 +33,14 @@
}); });
const openEditModal = (fruit: any) => { const openEditModal = (fruit: any) => {
selectedFruitId = fruit.id;
editForm = { ...fruit }; editForm = { ...fruit };
isEditModalOpen = true; isEditModalOpen = true;
}; };
const closeModal = () => { const closeModal = () => {
isEditModalOpen = false; isEditModalOpen = false;
selectedFruitId = null;
editForm = { editForm = {
id: '', id: '',
name: '', name: '',
@@ -85,7 +88,6 @@
}, 3000); }, 3000);
} }
} catch (error) { } catch (error) {
console.error('Error deleting devil fruit:', error);
saveMessage = { saveMessage = {
type: 'error', type: 'error',
text: 'Error deleting devil fruit' text: 'Error deleting devil fruit'
@@ -148,7 +150,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each filteredFruits as fruit (fruit.id)} {#each filteredFruits as fruit}
<tr class="border-b border-white/5 hover:bg-slate-800/50"> <tr class="border-b border-white/5 hover:bg-slate-800/50">
<td class="px-6 py-4 text-sm text-white">{fruit.name}</td> <td class="px-6 py-4 text-sm text-white">{fruit.name}</td>
<td class="px-6 py-4 text-sm"> <td class="px-6 py-4 text-sm">
@@ -231,7 +233,7 @@
bind:value={editForm.type} bind:value={editForm.type}
class="mt-1 w-full rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600" class="mt-1 w-full rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
> >
{#each fruitTypes as type (type)} {#each fruitTypes as type}
<option value={type}>{type}</option> <option value={type}>{type}</option>
{/each} {/each}
</select> </select>

View File

@@ -13,6 +13,7 @@
let isEditModalOpen = $state(false); let isEditModalOpen = $state(false);
let isSaving = $state(false); let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; message: string } | null>(null); let saveMessage = $state<{ type: 'success' | 'error'; message: string } | null>(null);
let selectedUserId = $state<string | null>(null);
let editForm = $state<any>({ let editForm = $state<any>({
id: '', id: '',
@@ -34,6 +35,7 @@
}); });
const openEditModal = (usr: any) => { const openEditModal = (usr: any) => {
selectedUserId = usr.id;
editForm = { ...usr }; editForm = { ...usr };
isEditModalOpen = true; isEditModalOpen = true;
saveMessage = null; saveMessage = null;
@@ -41,6 +43,7 @@
const closeModal = () => { const closeModal = () => {
isEditModalOpen = false; isEditModalOpen = false;
selectedUserId = null;
editForm = { editForm = {
id: '', id: '',
name: '', name: '',
@@ -117,7 +120,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each filteredUsers as usr (usr.id)} {#each filteredUsers as usr}
<tr class="border-b border-white/5 hover:bg-slate-800/50"> <tr class="border-b border-white/5 hover:bg-slate-800/50">
<td class="px-6 py-4 text-sm text-white">{usr.name}</td> <td class="px-6 py-4 text-sm text-white">{usr.name}</td>
<td class="px-6 py-4 text-sm text-gray-400">{usr.email}</td> <td class="px-6 py-4 text-sm text-gray-400">{usr.email}</td>

View File

@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import ProfileButton from '$lib/components/ProfileButton.svelte'; import ProfileButton from '$lib/components/ProfileButton.svelte';
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
import { resolve } from '$app/paths';
let { children, data } = $props(); let { children, data } = $props();
</script> </script>
@@ -9,13 +7,10 @@
<div class="min-h-screen bg-slate-950"> <div class="min-h-screen bg-slate-950">
<header class="fixed top-0 right-0 left-0 z-50 border-b border-white/5 bg-slate-950/95 backdrop-blur"> <header class="fixed top-0 right-0 left-0 z-50 border-b border-white/5 bg-slate-950/95 backdrop-blur">
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4"> <div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<a href={resolve("/")} class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100"> <a href="/" class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100">
OnePieceDle OnePieceDle
</a> </a>
<div class="flex items-center gap-3"> <ProfileButton user={data.user} />
<LanguageSwitcher />
<ProfileButton user={data.user} />
</div>
</div> </div>
</header> </header>
<main class="pt-20"> <main class="pt-20">

View File

@@ -1,59 +1,7 @@
<script lang="ts"> <script lang="ts">
export let data; export let data;
import { resolve } from '$app/paths';
import { language, t } from '$lib/i18n';
import type { CharacterWithRelations } from '$lib/server/daily-character';
$: yesterdayCharacter = data.yesterdayCharacter; $: yesterdayCharacter = data.yesterdayCharacter;
$: isFrench = $language === 'fr';
function parseEpithets(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
} catch {
if (value.length > 0) {
return [value];
}
}
}
return [];
}
function getDisplayName(character: CharacterWithRelations | null): string {
if (isFrench && typeof character?.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character?.name || '';
}
function getDisplayEpithets(character: CharacterWithRelations | null): string[] {
const frenchEpithets = parseEpithets(character?.frEpithets);
if (isFrench && frenchEpithets.length > 0) {
return frenchEpithets;
}
return parseEpithets(character?.epithets);
}
function getWikiUrl(character: CharacterWithRelations | null): string {
if (isFrench && typeof character?.frUrl === 'string' && character.frUrl.length > 0) {
return character.frUrl;
}
return character?.url || '';
}
</script> </script>
<svelte:head> <svelte:head>
@@ -63,7 +11,7 @@
<main <main
class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100" class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"
> >
<div class="absolute inset-0 bg-linear-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 w-full max-w-6xl flex-col items-center justify-center px-6 py-10"> <div class="relative mx-auto flex w-full max-w-6xl flex-col items-center justify-center px-6 py-10">
@@ -73,30 +21,30 @@
OnePieceDle OnePieceDle
</h1> </h1>
<p class="mt-4 max-w-2xl text-base text-slate-200 sm:text-lg"> <p class="mt-4 max-w-2xl text-base text-slate-200 sm:text-lg">
{$t.game.home.heroDescription} Devine le personnage de l'equipage, des marines ou du vaste monde. Chaque indice te rapproche du tresor.
</p> </p>
</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">{$t.game.home.dailyTitle}</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">{$t.game.home.dailySubtitle}</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">{$t.game.home.dailyDescription}</p> <p class="mt-2 text-sm text-slate-200">Compare tes essais, debloque des indices et garde ta serie.</p>
<a <a
href={resolve("/daily")} href="/daily"
class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200" class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200"
> >
{$t.game.home.dailyCta} Commencer
</a> </a>
</div> </div>
<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">{$t.game.home.infiniteTitle}</h2> <h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Mode Infini</h2>
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.infiniteSubtitle}</p> <p class="mt-3 text-lg font-semibold text-white">Des defis sans fin</p>
<p class="mt-2 text-sm text-slate-200">{$t.game.home.infiniteDescription}</p> <p class="mt-2 text-sm text-slate-200">Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.</p>
<a <a
href={resolve("/infinite")} href="/infinite"
class="mt-5 inline-flex w-full items-center justify-center 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" class="mt-5 inline-flex w-full items-center justify-center 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"
> >
{$t.game.home.infiniteCta} Jouer
</a> </a>
</div> </div>
</div> </div>
@@ -106,52 +54,43 @@
{#if yesterdayCharacter.pictureUrl} {#if yesterdayCharacter.pictureUrl}
<img <img
src={yesterdayCharacter.pictureUrl} src={yesterdayCharacter.pictureUrl}
alt={getDisplayName(yesterdayCharacter)} alt={yesterdayCharacter.name}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover" class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/> />
{:else} {: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"> <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">
{$t.game.home.photoFallback} Photo
</div> </div>
{/if} {/if}
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</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">{getDisplayName(yesterdayCharacter)}</p> <p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if getDisplayEpithets(yesterdayCharacter).length > 0} {#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400"> <p class="mt-1 text-sm text-slate-400">
{getDisplayEpithets(yesterdayCharacter).join(', ')} {typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
</p> </p>
{/if} {/if}
</div> </div>
{#if isFrench} <a
<a href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
href="https://onepiece.fandom.com/fr/wiki/{getWikiUrl(yesterdayCharacter)}" target="_blank"
target="_blank" rel="noopener noreferrer"
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"
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
{$t.game.home.openPage} </a>
</a>
{:else}
<a
href="https://onepiece.fandom.com/wiki/{getWikiUrl(yesterdayCharacter)}"
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"
>
{$t.game.home.openPage}
</a>
{/if}
</div> </div>
{:else} {: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">
{$t.game.home.photoFallback} Photo
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</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">{$t.game.home.noCharacter}</p> <p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">{$t.game.home.noYesterdayCharacter}</p> <p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -1,10 +1,10 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { character, characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema'; import { config } from '$lib/server/db/schema';
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character'; import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter } from '$lib/server/daily-character';
import { and, eq, inArray, like, or } from 'drizzle-orm'; import { like } from 'drizzle-orm';
export async function load(event) { export async function load() {
const characters = await getDailyModeCharacters(); const characters = await getDailyModeCharacters();
const dailyCharacter = await getOrCreateTodayCharacter(characters); const dailyCharacter = await getOrCreateTodayCharacter(characters);
@@ -14,97 +14,6 @@ export async function load(event) {
const yesterdayCharacter = await getYesterdayCharacter(new Date(), characters); const yesterdayCharacter = await getYesterdayCharacter(new Date(), characters);
// Load the win count for today
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
let friendsTodayResults: Array<{
userId: string;
name: string;
image: string | null;
tryCount: number;
triedCharacters: Array<{ id: string; name: string; pictureUrl: string | null }>;
}> = [];
if (event.locals.user) {
const currentUserId = event.locals.user.id;
const acceptedFriendships = await db
.select({
requesterId: friendship.requesterId,
addresseeId: friendship.addresseeId
})
.from(friendship)
.where(
and(
eq(friendship.status, 'accepted'),
or(eq(friendship.requesterId, currentUserId), eq(friendship.addresseeId, currentUserId))
)
);
const friendIds = acceptedFriendships.map((relation) =>
relation.requesterId === currentUserId ? relation.addresseeId : relation.requesterId
);
if (friendIds.length > 0) {
const todayDate = getDateKey(new Date());
const [todayHistoryEntry] = await db
.select({ id: characterHistory.id })
.from(characterHistory)
.where(eq(characterHistory.date, todayDate))
.limit(1);
const todayCharacterHistoryId = todayHistoryEntry?.id;
if (todayCharacterHistoryId) {
const friendResultsRaw = await db
.select({
userId: user.id,
name: user.name,
image: user.image,
tryCount: userCharacterHistory.tryCount,
triedCharacterIds: userCharacterHistory.triedCharacterIds
})
.from(userCharacterHistory)
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
.where(
and(
eq(userCharacterHistory.characterHistoryId, todayCharacterHistoryId),
inArray(userCharacterHistory.userId, friendIds)
)
)
.orderBy(userCharacterHistory.tryCount);
const uniqueTriedCharacterIds = Array.from(new Set(
friendResultsRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
));
const triedCharacters = uniqueTriedCharacterIds.length > 0
? await db
.select({
id: character.id,
name: character.name,
pictureUrl: character.pictureUrl
})
.from(character)
.where(inArray(character.id, uniqueTriedCharacterIds))
: [];
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
friendsTodayResults = friendResultsRaw.map((entry) => ({
userId: entry.userId,
name: entry.name,
image: entry.image,
tryCount: entry.tryCount,
triedCharacters: (entry.triedCharacterIds ?? [])
.map((characterId) => triedCharactersById.get(characterId))
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
}));
}
}
}
// Load column visibility config // Load column visibility config
const columnConfig = await db const columnConfig = await db
.select() .select()
@@ -124,8 +33,6 @@ export async function load(event) {
characters, characters,
dailyCharacter, dailyCharacter,
yesterdayCharacter, yesterdayCharacter,
columnVisibility, columnVisibility
winCount,
friendsTodayResults
}; };
} }

View File

@@ -1,98 +1,23 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte'; import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte'; import HintsPanel from '$lib/components/HintsPanel.svelte';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte'; import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte'; import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
import WinPanel from '$lib/components/WinPanel.svelte'; import WinPanel from '$lib/components/WinPanel.svelte';
import FriendsTodaySection from '$lib/components/FriendsTodaySection.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
import { t } from '$lib/i18n';
export let data; export let data;
let selectedCharacters: CharacterWithRelations[] = []; let selectedCharacters: any[] = [];
let isLoaded = false; let isLoaded = false;
let isGeckoMoriaWin = false; let isGeckoMoriaWin = false;
let wasOriginAvailable = false;
let wasFruitAvailable = false;
let wasAffiliationAvailable = false;
let showOriginUnlock = false; let showOriginUnlock = false;
let showFruitUnlock = false; let showFruitUnlock = false;
let showAffiliationUnlock = false; let showAffiliationUnlock = false;
let originUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let fruitUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let affiliationUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
function clearUnlockTimeout(timeout: ReturnType<typeof setTimeout> | null) {
if (timeout) {
clearTimeout(timeout);
}
}
function pulseUnlock(type: 'origin' | 'fruit' | 'affiliation') {
if (type === 'origin') {
clearUnlockTimeout(originUnlockTimeout);
showOriginUnlock = true;
originUnlockTimeout = setTimeout(() => {
showOriginUnlock = false;
originUnlockTimeout = null;
}, 600);
return;
}
if (type === 'fruit') {
clearUnlockTimeout(fruitUnlockTimeout);
showFruitUnlock = true;
fruitUnlockTimeout = setTimeout(() => {
showFruitUnlock = false;
fruitUnlockTimeout = null;
}, 600);
return;
}
clearUnlockTimeout(affiliationUnlockTimeout);
showAffiliationUnlock = true;
affiliationUnlockTimeout = setTimeout(() => {
showAffiliationUnlock = false;
affiliationUnlockTimeout = null;
}, 600);
}
function syncHintAvailability(previousGuessCount: number, nextGuessCount: number, animateUnlocks = false) {
const nextOriginAvailable = nextGuessCount >= 5;
const nextFruitAvailable = nextGuessCount >= 10;
const nextAffiliationAvailable = nextGuessCount >= 15;
if (animateUnlocks && nextOriginAvailable && previousGuessCount < 5) {
pulseUnlock('origin');
}
if (animateUnlocks && nextFruitAvailable && previousGuessCount < 10) {
pulseUnlock('fruit');
}
if (animateUnlocks && nextAffiliationAvailable && previousGuessCount < 15) {
pulseUnlock('affiliation');
}
if (!nextOriginAvailable) {
showOriginUnlock = false;
clearUnlockTimeout(originUnlockTimeout);
originUnlockTimeout = null;
}
if (!nextFruitAvailable) {
showFruitUnlock = false;
clearUnlockTimeout(fruitUnlockTimeout);
fruitUnlockTimeout = null;
}
if (!nextAffiliationAvailable) {
showAffiliationUnlock = false;
clearUnlockTimeout(affiliationUnlockTimeout);
affiliationUnlockTimeout = null;
}
}
// Load from localStorage on mount // Load from localStorage on mount
onMount(() => { onMount(() => {
@@ -112,8 +37,8 @@
// Reconstruct character objects from IDs // Reconstruct character objects from IDs
if (Array.isArray(storedIds)) { if (Array.isArray(storedIds)) {
selectedCharacters = storedIds selectedCharacters = storedIds
.map((id: string) => data.characters.find((c: CharacterWithRelations) => c.id === id)) .map((id: string) => data.characters.find((c: any) => c.id === id))
.filter((c: CharacterWithRelations | undefined): c is CharacterWithRelations => !!c); .filter((c: any) => c !== undefined);
} }
} catch (e) { } catch (e) {
console.error('Failed to parse stored history', e); console.error('Failed to parse stored history', e);
@@ -126,17 +51,9 @@
localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId); localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId);
} }
syncHintAvailability(0, selectedCharacters.length);
isLoaded = true; isLoaded = true;
}); });
onDestroy(() => {
clearUnlockTimeout(originUnlockTimeout);
clearUnlockTimeout(fruitUnlockTimeout);
clearUnlockTimeout(affiliationUnlockTimeout);
});
// Save to localStorage whenever selectedCharacters changes (only store IDs) // Save to localStorage whenever selectedCharacters changes (only store IDs)
$: if (isLoaded && selectedCharacters) { $: if (isLoaded && selectedCharacters) {
const ids = selectedCharacters.map(char => char.id); const ids = selectedCharacters.map(char => char.id);
@@ -149,19 +66,42 @@
$: columnVisibility = data.columnVisibility || {}; $: columnVisibility = data.columnVisibility || {};
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id); $: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
function handleCharacterSelect(character: CharacterWithRelations) { // Hint availability tracking for unlock animations
$: isOriginAvailable = selectedCharacters.length >= 5;
$: isFruitAvailable = selectedCharacters.length >= 10;
$: isAffiliationAvailable = selectedCharacters.length >= 15;
// 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;
}
function handleCharacterSelect(event: CustomEvent) {
const character = event.detail;
selectCharacter(character); selectCharacter(character);
} }
function selectCharacter(character: CharacterWithRelations) { function selectCharacter(character: any) {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [character, ...selectedCharacters]; selectedCharacters = [character, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
// Check if player won // Check if player won
if (character.id === dailyCharacter.id) { if (character.id === dailyCharacter.id) {
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
// Send request to record win in database // Send request to record win in database
fetch('/daily', { fetch('/daily', {
method: 'POST', method: 'POST',
@@ -169,9 +109,7 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
characterId: dailyCharacter.id, characterId: dailyCharacter.id
tryCount: selectedCharacters.length,
triedCharacterIds
}) })
}).catch(err => console.error('Failed to record win:', err)); }).catch(err => console.error('Failed to record win:', err));
@@ -183,15 +121,13 @@
} }
function resetHistory() { function resetHistory() {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = []; selectedCharacters = [];
syncHintAvailability(previousGuessCount, 0);
localStorage.removeItem('dailyCharacterHistory'); localStorage.removeItem('dailyCharacterHistory');
} }
</script> </script>
<svelte:head> <svelte:head>
<title>{$t.game.daily.metaTitle}</title> <title>OnePieceDle - Mode du jour</title>
<style> <style>
@keyframes shadow-pulse { @keyframes shadow-pulse {
0% { 0% {
@@ -261,31 +197,26 @@
<main <main
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}" class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}"
> >
<div class="absolute inset-0 bg-linear-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-8 sm:py-10"> <div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-8 sm:py-10">
<header class="flex flex-col items-start gap-6 w-full"> <header class="flex flex-col items-start gap-6 w-full">
<div class="flex w-full items-center justify-between gap-4"> <div class="flex w-full items-center justify-between gap-4">
<div> <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"> Personnage du jour
{$t.game.daily.title} </h1>
</h1>
<p class="mt-2 text-sm text-amber-300">
{data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular} {data.winCount > 1 ? $t.game.daily.winsVerbPlural : $t.game.daily.winsVerbSingular} {$t.game.daily.winsSuffix}
</p>
</div>
{#if hasWon} {#if hasWon}
<button <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" 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} onclick={resetHistory}
> >
{$t.game.daily.reset} Recommencer
</button> </button>
{/if} {/if}
</div> </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">
{$t.game.daily.description} Devine le personnage. Chaque indice se débloque après un certain nombre de tentatives. Bonne chance !
</p> </p>
</header> </header>
@@ -302,7 +233,7 @@
{#if hasWon} {#if hasWon}
<WinPanel <WinPanel
selectedCharacter={dailyCharacter} {dailyCharacter}
{selectedCharacters} {selectedCharacters}
{isGeckoMoriaWin} {isGeckoMoriaWin}
/> />
@@ -310,16 +241,11 @@
<CharacterSearchInput <CharacterSearchInput
{characters} {characters}
{selectedCharacters} {selectedCharacters}
onSelect={handleCharacterSelect} on:select={handleCharacterSelect}
/> />
{/if} {/if}
</section> </section>
{#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0}
<FriendsTodaySection friendsTodayResults={data.friendsTodayResults} />
{/if}
<GuessHistoryTable <GuessHistoryTable
{selectedCharacters} {selectedCharacters}
{dailyCharacter} {dailyCharacter}

View File

@@ -1,16 +1,13 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { characterHistory, userCharacterHistory } from '$lib/server/db/schema'; import { characterHistory } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { getDateKey } from '$lib/server/daily-character'; import { getDateKey } from '$lib/server/daily-character';
export async function POST({ request, locals }) { export async function POST({ request }) {
try { try {
const { characterId, tryCount, triedCharacterIds } = await request.json(); const { characterId } = await request.json();
const normalizedTriedCharacterIds = Array.isArray(triedCharacterIds)
? triedCharacterIds.filter((id): id is string => typeof id === 'string')
: [];
if (!characterId) { if (!characterId) {
return json({ error: 'Missing characterId' }, { status: 400 }); return json({ error: 'Missing characterId' }, { status: 400 });
@@ -18,56 +15,14 @@ export async function POST({ request, locals }) {
const todayDate = getDateKey(new Date()); const todayDate = getDateKey(new Date());
// If user is logged in, check if they already played today // Increment the won counter for today's entry
if (locals.user) { await db
// Get the characterHistoryId for today .update(characterHistory)
const [todayHistoryEntry] = await db .set({
.select({ id: characterHistory.id }) won: sql`${characterHistory.won} + 1`,
.from(characterHistory) updatedAt: Date.now()
.where(eq(characterHistory.date, todayDate)); })
.where(eq(characterHistory.date, todayDate));
if (todayHistoryEntry) {
// Check if user already has a record for today
const [existingRecord] = await db
.select()
.from(userCharacterHistory)
.where(and(
eq(userCharacterHistory.userId, locals.user.id),
eq(userCharacterHistory.characterHistoryId, todayHistoryEntry.id)
));
// If user already played today, don't record again
if (existingRecord) {
return json({ success: false, message: 'Already played today' });
}
// 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));
// Insert into userCharacterHistory
await db.insert(userCharacterHistory).values({
userId: locals.user.id,
characterHistoryId: todayHistoryEntry.id,
tryCount: tryCount,
triedCharacterIds: normalizedTriedCharacterIds
});
}
} else {
// If user is not logged in, always increment counter
await db
.update(characterHistory)
.set({
won: sql`${characterHistory.won} + 1`,
updatedAt: Date.now()
})
.where(eq(characterHistory.date, todayDate));
}
return json({ success: true }); return json({ success: true });
} catch (error) { } catch (error) {

View File

@@ -1,117 +1,34 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import { formatBounty } from '$lib';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte'; import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte'; import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
import WinPanel from '$lib/components/WinPanel.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
import { language, t } from '$lib/i18n';
export let data; export let data;
let selectedCharacters: CharacterWithRelations[] = []; let selectedCharacters: any[] = [];
let currentCharacter: CharacterWithRelations | null = null; let currentCharacter: any = null;
let isLoaded = false; let isLoaded = false;
let score = 0; let score = 0;
type ArcFilterOption = { id: string; name: string };
let allCharacters: CharacterWithRelations[] = [];
let characters: CharacterWithRelations[] = [];
let availableArcs: ArcFilterOption[] = [];
let hasWon = false;
let columnVisibility: Record<string, boolean> = {}; let columnVisibility: Record<string, boolean> = {};
let columnDisplayNames: Record<string, string> = {}; const columnDisplayNames: Record<string, string> = {
status: 'Statut',
// Character filters gender: 'Genre',
let characterFilters = { affiliations: 'Affiliations',
gender: [] as string[], devilFruitType: 'Fruit',
hasHaki: false, haki: 'Haki',
hasDevilFruit: null as boolean | null, // null = all, true = with fruit, false = without fruit bounty: 'Prime',
status: [] as string[], height: 'Taille',
hasHeight: false, origin: 'Origine',
hasAge: false, arc: 'Arc'
hasOrigin: false,
arcs: [] as string[]
}; };
let wasOriginAvailable = false;
let wasFruitAvailable = false;
let wasAffiliationAvailable = false;
let showOriginUnlock = false; let showOriginUnlock = false;
let showFruitUnlock = false; let showFruitUnlock = false;
let showAffiliationUnlock = false; let showAffiliationUnlock = false;
let isGeckoMoriaWin = false;
let originUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let fruitUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let affiliationUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
function clearUnlockTimeout(timeout: ReturnType<typeof setTimeout> | null) {
if (timeout) {
clearTimeout(timeout);
}
}
function pulseUnlock(type: 'origin' | 'fruit' | 'affiliation') {
if (type === 'origin') {
clearUnlockTimeout(originUnlockTimeout);
showOriginUnlock = true;
originUnlockTimeout = setTimeout(() => {
showOriginUnlock = false;
originUnlockTimeout = null;
}, 600);
return;
}
if (type === 'fruit') {
clearUnlockTimeout(fruitUnlockTimeout);
showFruitUnlock = true;
fruitUnlockTimeout = setTimeout(() => {
showFruitUnlock = false;
fruitUnlockTimeout = null;
}, 600);
return;
}
clearUnlockTimeout(affiliationUnlockTimeout);
showAffiliationUnlock = true;
affiliationUnlockTimeout = setTimeout(() => {
showAffiliationUnlock = false;
affiliationUnlockTimeout = null;
}, 600);
}
function syncHintAvailability(previousGuessCount: number, nextGuessCount: number, animateUnlocks = false) {
const nextOriginAvailable = nextGuessCount >= 5;
const nextFruitAvailable = nextGuessCount >= 10;
const nextAffiliationAvailable = nextGuessCount >= 15;
if (animateUnlocks && nextOriginAvailable && previousGuessCount < 5) {
pulseUnlock('origin');
}
if (animateUnlocks && nextFruitAvailable && previousGuessCount < 10) {
pulseUnlock('fruit');
}
if (animateUnlocks && nextAffiliationAvailable && previousGuessCount < 15) {
pulseUnlock('affiliation');
}
if (!nextOriginAvailable) {
showOriginUnlock = false;
clearUnlockTimeout(originUnlockTimeout);
originUnlockTimeout = null;
}
if (!nextFruitAvailable) {
showFruitUnlock = false;
clearUnlockTimeout(fruitUnlockTimeout);
fruitUnlockTimeout = null;
}
if (!nextAffiliationAvailable) {
showAffiliationUnlock = false;
clearUnlockTimeout(affiliationUnlockTimeout);
affiliationUnlockTimeout = null;
}
}
// Load from localStorage on mount // Load from localStorage on mount
onMount(() => { onMount(() => {
@@ -126,30 +43,12 @@
try { try {
columnVisibility = JSON.parse(storedColumnVisibility); columnVisibility = JSON.parse(storedColumnVisibility);
} catch (e) { } catch (e) {
console.error('Failed to parse column visibility', e);
columnVisibility = data.columnVisibility || {}; columnVisibility = data.columnVisibility || {};
} }
} else { } else {
columnVisibility = data.columnVisibility || {}; columnVisibility = data.columnVisibility || {};
} }
// Load character filters from localStorage
const storedFilters = localStorage.getItem('infiniteCharacterFilters');
if (storedFilters) {
try {
characterFilters = JSON.parse(storedFilters);
// Ensure all filter properties exist
if (!characterFilters.arcs) {
characterFilters.arcs = [];
}
if (typeof characterFilters.hasAge !== 'boolean') {
characterFilters.hasAge = false;
}
} catch (e) {
console.error('Failed to parse filters', e);
}
}
// Load current character ID and history IDs from localStorage // Load current character ID and history IDs from localStorage
const storedCharacterId = localStorage.getItem('infiniteCurrentCharacterId'); const storedCharacterId = localStorage.getItem('infiniteCurrentCharacterId');
const storedHistoryIds = localStorage.getItem('infiniteSelectedCharacterIds'); const storedHistoryIds = localStorage.getItem('infiniteSelectedCharacterIds');
@@ -160,19 +59,18 @@
const historyIds = JSON.parse(storedHistoryIds); const historyIds = JSON.parse(storedHistoryIds);
// Find the character object by ID // Find the character object by ID
currentCharacter = characters.find((c: CharacterWithRelations) => c.id === charId) || null; currentCharacter = characters.find((c: any) => c.id === charId);
// Find all character objects by their IDs // Find all character objects by their IDs
selectedCharacters = historyIds selectedCharacters = historyIds
.map((id: string) => characters.find((c: CharacterWithRelations) => c.id === id)) .map((id: string) => characters.find((c: any) => c.id === id))
.filter((c: CharacterWithRelations | undefined) => !!c) as CharacterWithRelations[]; .filter((c: any) => c !== undefined);
// If character not found, generate a new one // If character not found, generate a new one
if (!currentCharacter) { if (!currentCharacter) {
generateNewCharacter(); generateNewCharacter();
} }
} catch (e) { } catch (e) {
console.error('Failed to parse character data', e);
// If parsing fails, generate a new character // If parsing fails, generate a new character
generateNewCharacter(); generateNewCharacter();
} }
@@ -180,16 +78,9 @@
generateNewCharacter(); generateNewCharacter();
} }
syncHintAvailability(0, selectedCharacters.length);
isLoaded = true; isLoaded = true;
}); });
onDestroy(() => {
clearUnlockTimeout(originUnlockTimeout);
clearUnlockTimeout(fruitUnlockTimeout);
clearUnlockTimeout(affiliationUnlockTimeout);
});
// Save score to localStorage whenever it changes // Save score to localStorage whenever it changes
$: if (isLoaded) { $: if (isLoaded) {
localStorage.setItem('infiniteScore', score.toString()); localStorage.setItem('infiniteScore', score.toString());
@@ -200,11 +91,6 @@
localStorage.setItem('infiniteColumnVisibility', JSON.stringify(columnVisibility)); localStorage.setItem('infiniteColumnVisibility', JSON.stringify(columnVisibility));
} }
// Save character filters to localStorage whenever they change
$: if (isLoaded) {
localStorage.setItem('infiniteCharacterFilters', JSON.stringify(characterFilters));
}
// Save current character ID to localStorage whenever it changes // Save current character ID to localStorage whenever it changes
$: if (isLoaded && currentCharacter) { $: if (isLoaded && currentCharacter) {
localStorage.setItem('infiniteCurrentCharacterId', JSON.stringify(currentCharacter.id)); localStorage.setItem('infiniteCurrentCharacterId', JSON.stringify(currentCharacter.id));
@@ -212,137 +98,55 @@
// Save selected character IDs to localStorage whenever it changes // Save selected character IDs to localStorage whenever it changes
$: if (isLoaded) { $: if (isLoaded) {
const selectedIds = selectedCharacters.map((c: CharacterWithRelations) => c.id); const selectedIds = selectedCharacters.map((c: any) => c.id);
localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds)); localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds));
} }
$: allCharacters = data.characters || []; $: characters = data.characters || [];
$: isFrench = $language === 'fr'; $: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id);
function getDisplayArcName(character: CharacterWithRelations, useFrench: boolean): string | null { // Hint availability tracking for unlock animations
if (useFrench && typeof character.frArcName === 'string' && character.frArcName.length > 0) { $: isOriginAvailable = selectedCharacters.length >= 5;
return character.frArcName; $: isFruitAvailable = selectedCharacters.length >= 10;
$: isAffiliationAvailable = selectedCharacters.length >= 15;
// Track hint unlocks
$: if (isLoaded) {
if (isOriginAvailable && !wasOriginAvailable) {
showOriginUnlock = true;
setTimeout(() => (showOriginUnlock = false), 600);
} }
wasOriginAvailable = isOriginAvailable;
return character.arcName; if (isFruitAvailable && !wasFruitAvailable) {
showFruitUnlock = true;
setTimeout(() => (showFruitUnlock = false), 600);
}
wasFruitAvailable = isFruitAvailable;
if (isAffiliationAvailable && !wasAffiliationAvailable) {
showAffiliationUnlock = true;
setTimeout(() => (showAffiliationUnlock = false), 600);
}
wasAffiliationAvailable = isAffiliationAvailable;
} }
// Extract unique arcs from all characters
$: {
const useFrench = isFrench;
const arcMap = new Map<string, ArcFilterOption>(
allCharacters
.filter(
(char: CharacterWithRelations): char is CharacterWithRelations & { arcId: string } =>
typeof char.arcId === 'string' &&
char.arcId.length > 0 &&
typeof getDisplayArcName(char, useFrench) === 'string' &&
getDisplayArcName(char, useFrench)!.length > 0
)
.map((char: CharacterWithRelations & { arcId: string }) => [
char.arcId,
{ id: char.arcId, name: getDisplayArcName(char, useFrench) as string }
])
);
availableArcs = [...arcMap.values()].sort((a, b) => a.name.localeCompare(b.name));
}
// Filter characters based on selected filters
$: characters = allCharacters.filter((char: CharacterWithRelations) => {
// Gender filter
if (characterFilters.gender.length > 0 && (char.gender == null || !characterFilters.gender.includes(char.gender))) {
return false;
}
// Haki filter
if (characterFilters.hasHaki && !(char.hakiObservation || char.hakiArmament || char.hakiConqueror)) {
return false;
}
// Devil fruit filter
if (characterFilters.hasDevilFruit !== null) {
const hasDevil = char.devilFruitId !== null && char.devilFruitId !== undefined;
if (characterFilters.hasDevilFruit !== hasDevil) {
return false;
}
}
// Status filter
if (characterFilters.status.length > 0) {
const normalizedStatus = normalizeStatus(char.status);
if (!characterFilters.status.includes(normalizedStatus)) {
return false;
}
}
// Height filter
if (characterFilters.hasHeight && (char.height === null || char.height === undefined)) {
return false;
}
// Age filter
if (characterFilters.hasAge && (char.age === null || char.age === undefined)) {
return false;
}
// Origin filter
if (characterFilters.hasOrigin && (char.origin === null || char.origin === undefined || char.origin === '')) {
return false;
}
// Arc filter
if (characterFilters.arcs.length > 0 && (char.arcId == null || !characterFilters.arcs.includes(char.arcId))) {
return false;
}
return true;
});
$: {
const currentCharacterId = currentCharacter?.id;
hasWon = currentCharacterId != null && selectedCharacters.some(char => char.id === currentCharacterId);
}
$: if (hasWon && currentCharacter?.id === 'gecko_moria_gecko_moria') {
isGeckoMoriaWin = true;
} else if (!hasWon) {
isGeckoMoriaWin = false;
}
$: columnDisplayNames = {
status: $t.game.components.guessHistory.status,
gender: $t.game.components.guessHistory.gender,
affiliations: $t.game.components.guessHistory.affiliations,
devilFruitType: $t.game.components.guessHistory.fruit,
haki: $t.game.components.guessHistory.haki,
bounty: $t.game.components.guessHistory.bounty,
height: $t.game.components.guessHistory.height,
age: $t.game.components.guessHistory.age,
origin: $t.game.components.guessHistory.origin,
arc: $t.game.components.guessHistory.arc
};
function generateNewCharacter() { function generateNewCharacter() {
if (characters.length === 0) return; if (characters.length === 0) return;
currentCharacter = characters[Math.floor(Math.random() * characters.length)]; currentCharacter = characters[Math.floor(Math.random() * characters.length)];
syncHintAvailability(selectedCharacters.length, 0);
selectedCharacters = []; selectedCharacters = [];
} }
function handleCharacterSelect(character: CharacterWithRelations) { function handleCharacterSelect(event: CustomEvent) {
const character = event.detail;
selectCharacter(character); selectCharacter(character);
} }
function selectCharacter(character: CharacterWithRelations) { function selectCharacter(character: any) {
const current = currentCharacter;
if (!current) {
return;
}
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [character, ...selectedCharacters]; selectedCharacters = [character, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
// Check if player won // Check if player won
if (character.id === current.id) { if (character.id === currentCharacter.id) {
// Increment score (saved to localStorage via reactive statement) // Increment score (saved to localStorage via reactive statement)
score++; score++;
// Don't auto-generate next character - wait for user to click "Recommencer" // Don't auto-generate next character - wait for user to click "Recommencer"
@@ -366,301 +170,195 @@
columnVisibility[column] = !columnVisibility[column]; columnVisibility[column] = !columnVisibility[column];
columnVisibility = columnVisibility; // Trigger reactivity columnVisibility = columnVisibility; // Trigger reactivity
} }
function revealAnswer() {
if (!currentCharacter) {
return;
}
// Reset score (strike)
score = 0;
// Add the current character as the correct answer
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [currentCharacter, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
}
function toggleGenderFilter(gender: string) {
if (characterFilters.gender.includes(gender)) {
characterFilters.gender = characterFilters.gender.filter(g => g !== gender);
} else {
characterFilters.gender = [...characterFilters.gender, gender];
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleStatusFilter(status: string) {
if (characterFilters.status.includes(status)) {
characterFilters.status = characterFilters.status.filter(s => s !== status);
} else {
characterFilters.status = [...characterFilters.status, status];
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleHakiFilter() {
characterFilters.hasHaki = !characterFilters.hasHaki;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleDevilFruitFilter() {
if (characterFilters.hasDevilFruit === null) {
characterFilters.hasDevilFruit = true;
} else if (characterFilters.hasDevilFruit === true) {
characterFilters.hasDevilFruit = false;
} else {
characterFilters.hasDevilFruit = null;
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleHeightFilter() {
characterFilters.hasHeight = !characterFilters.hasHeight;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleAgeFilter() {
characterFilters.hasAge = !characterFilters.hasAge;
if (!hasWon) {
generateNewCharacter();
}
}
function toggleOriginFilter() {
characterFilters.hasOrigin = !characterFilters.hasOrigin;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleArcFilter(arcId: string) {
if (characterFilters.arcs.includes(arcId)) {
characterFilters.arcs = characterFilters.arcs.filter(a => a !== arcId);
} else {
characterFilters.arcs = [...characterFilters.arcs, arcId];
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function clearAllFilters() {
characterFilters = {
gender: [],
hasHaki: false,
hasDevilFruit: null,
status: [],
hasHeight: false,
hasAge: false,
hasOrigin: false,
arcs: []
};
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function normalizeStatus(status: unknown): string {
if (status == null) {
return 'Unknown';
}
if (typeof status !== 'string') {
return String(status);
}
const value = status.trim();
if (value === '') {
return 'Unknown';
}
switch (value.toLowerCase()) {
case 'alive':
return 'Alive';
case 'dead':
case 'deceased':
return 'Dead';
case 'unknown':
case 'inconnu':
case '-':
return 'Unknown';
default:
return value;
}
}
</script> </script>
<svelte:head> <svelte:head>
<title>{$t.game.infinite.metaTitle}</title> <title>OnePieceDle - Mode Infini</title>
<style> <style>
@keyframes shadow-pulse { @keyframes shadow-pulse {
0% { 0% {
text-shadow: text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
0 0 20px rgba(0, 0, 0, 0.5),
0 0 40px rgba(50, 50, 50, 0.8);
opacity: 1; opacity: 1;
} }
50% { 50% {
text-shadow: text-shadow: 0 0 60px rgba(0, 0, 0, 0.9), 0 0 100px rgba(30, 30, 30, 1),
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); inset 0 0 50px rgba(0, 0, 0, 0.7);
opacity: 0.9; opacity: 0.9;
} }
100% { 100% {
text-shadow: text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
0 0 20px rgba(0, 0, 0, 0.5),
0 0 40px rgba(50, 50, 50, 0.8);
opacity: 1; opacity: 1;
} }
} }
.gecko-moria-effect { .gecko-moria-effect {
animation: shadow-pulse 1.5s ease-in-out infinite; animation: shadow-pulse 1.5s ease-in-out infinite;
} }
@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);
}
}
.moria-screen-chaos {
animation: moria-chaos 4s ease-in-out;
}
</style> </style>
</svelte:head> </svelte:head>
<main <main
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
? 'moria-screen-chaos'
: ''}"
> >
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
<div <div
class="absolute inset-0 bg-linear-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80" 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 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)] opacity-20 mix-blend-screen"
></div> ></div>
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-8 sm:py-10"> <div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-8 sm:py-10">
<header class="flex w-full flex-col items-start gap-6"> <header class="flex flex-col items-start gap-6 w-full">
<div class="flex w-full items-center justify-between gap-4"> <div class="flex w-full items-center justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-black tracking-[0.25em] text-amber-50 uppercase sm:text-5xl"> <h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
{$t.game.infinite.title} Mode Infini
</h1> </h1>
<p class="mt-2 text-2xl font-bold text-amber-300">{$t.game.infinite.score}: {score}</p> <p class="mt-2 text-2xl font-bold text-amber-300">Score: {score}</p>
</div> </div>
{#if score > 0} {#if score > 0}
<button <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" 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={resetScore} onclick={resetScore}
> >
{$t.game.infinite.resetScore} Réinitialiser
</button> </button>
{/if} {/if}
</div> </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">
{$t.game.infinite.description} Devine des personnages à l'infini ! 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 currentCharacter} {#if currentCharacter}
{#if hasWon} {#if hasWon}
<div> <div
<WinPanel selectedCharacter={currentCharacter} {selectedCharacters} {isGeckoMoriaWin} /> 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"
<button >
type="button" <div class="text-center">
onclick={nextCharacter} <div class="text-3xl mb-2">🎉</div>
class="mt-4 w-full rounded-full bg-emerald-500 px-6 py-2 text-sm font-semibold text-white transition hover:bg-emerald-600" <h2 class="text-xl font-bold text-emerald-400 mb-1">Bien joué !</h2>
> <p class="text-sm text-emerald-300">
{$t.game.infinite.nextCharacter} Vous avez trouvé le personnage en {selectedCharacters.length}
</button> {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !
</div> </p>
{:else} <div class="mt-3">
{#if selectedCharacters.length > 0} {#if currentCharacter.pictureUrl}
<HintsPanel <a
dailyCharacter={currentCharacter} href={'https://onepiece.fandom.com/fr/wiki/' + currentCharacter.url}
{selectedCharacters} target="_blank"
{showOriginUnlock} rel="noopener noreferrer"
{showFruitUnlock} class="inline-block"
{showAffiliationUnlock} >
/> <img
<div class="mt-2 flex justify-center"> src={currentCharacter.pictureUrl}
alt={currentCharacter.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">{currentCharacter.name}</p>
</div>
<button <button
type="button" type="button"
onclick={revealAnswer} onclick={nextCharacter}
class="rounded-lg border border-red-600/40 bg-red-900/20 px-4 py-2 text-sm text-red-300 transition hover:border-red-500 hover:bg-red-900/40 hover:text-red-200" class="mt-4 rounded-full bg-emerald-500 px-6 py-2 text-sm font-semibold text-white transition hover:bg-emerald-600"
> >
{$t.game.infinite.revealAnswer} Recommencer
</button> </button>
</div> </div>
{/if} </div>
{: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"
>
<div class="grid gap-3 sm:grid-cols-3">
<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 {isOriginAvailable
? 'bg-slate-950/60'
: 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showOriginUnlock
? 'hint-unlocking'
: ''}"
disabled={!isOriginAvailable}
onclick={() => (showOriginUnlock = !showOriginUnlock)}
>
<p class="text-sm font-medium text-amber-100">Origine</p>
{#if showOriginUnlock}
<p class="mt-2 text-xs text-white font-semibold">
{currentCharacter.origin || 'Inconnue'}
</p>
{:else if Math.max(0, 5 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">
{Math.max(0, 5 - 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 {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={() => (showFruitUnlock = !showFruitUnlock)}
>
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
{#if showFruitUnlock}
<p class="mt-2 text-xs text-white font-semibold">
{currentCharacter.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={() => (showAffiliationUnlock = !showAffiliationUnlock)}
>
<p class="text-sm font-medium text-amber-100">Affiliation</p>
{#if showAffiliationUnlock}
{@const affiliations = typeof currentCharacter.affiliations === 'string'
? currentCharacter.affiliations.includes('[')
? JSON.parse(currentCharacter.affiliations)
: currentCharacter.affiliations
.split(',')
.map((a: string) => a.trim())
: currentCharacter.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>
<CharacterSearchInput <CharacterSearchInput
{characters} {characters}
{selectedCharacters} {selectedCharacters}
onSelect={handleCharacterSelect} on:select={handleCharacterSelect}
/> />
{/if} {/if}
{:else} {:else}
<div <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">
class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur" <p class="text-center text-slate-300">Chargement du personnage...</p>
>
<p class="text-center text-slate-300">{$t.game.infinite.loadingCharacter}</p>
</div> </div>
{/if} {/if}
</section> </section>
@@ -672,169 +370,13 @@
{columnVisibility} {columnVisibility}
/> />
<!-- Character Filters -->
<section class="mt-6">
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4">
<div class="mb-3 flex items-center justify-between gap-3">
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
{$t.game.infinite.filtersTitle}
</h3>
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasAge || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
<button
type="button"
onclick={clearAllFilters}
class="text-xs text-red-300 transition hover:text-red-200"
>
{$t.game.infinite.clearFilters}
</button>
{/if}
</div>
<div class="space-y-3">
<!-- Gender Filter -->
<div>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterGender}</p>
<div class="flex flex-wrap gap-2">
{#each ['Male', 'Female'] as gender (gender)}
<button
type="button"
onclick={() => toggleGenderFilter(gender)}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.gender.includes(
gender
)
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{gender === 'Male' ? $t.game.infinite.male : $t.game.infinite.female}
</button>
{/each}
</div>
</div>
<!-- Status Filter -->
<div>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterStatus}</p>
<div class="flex flex-wrap gap-2">
{#each ['Alive', 'Dead', 'Unknown'] as status (status)}
<button
type="button"
onclick={() => toggleStatusFilter(status)}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.status.includes(
status
)
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{status === 'Alive' ? $t.game.infinite.alive : status === 'Dead' ? $t.game.infinite.dead : $t.game.infinite.unknown}
</button>
{/each}
</div>
</div>
<!-- Haki Filter -->
<div>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterAbilities}</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
onclick={toggleHakiFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasHaki
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{$t.game.infinite.hasHaki}
</button>
<button
type="button"
onclick={toggleDevilFruitFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasDevilFruit ===
true
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: characterFilters.hasDevilFruit === false
? 'border-purple-300/50 bg-purple-300/10 text-purple-100 hover:bg-purple-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{characterFilters.hasDevilFruit === null
? $t.game.infinite.fruitAll
: characterFilters.hasDevilFruit
? $t.game.infinite.withFruit
: $t.game.infinite.withoutFruit}
</button>
</div>
</div>
<!-- Informations Filter -->
<div>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterInformation}</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
onclick={toggleHeightFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasHeight
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{$t.game.infinite.heightDefined}
</button>
<button
type="button"
onclick={toggleAgeFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasAge
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{$t.game.infinite.ageDefined}
</button>
<button
type="button"
onclick={toggleOriginFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasOrigin
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{$t.game.infinite.originDefined}
</button>
</div>
</div>
<!-- Arc Filter -->
<div>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterArcs}</p>
<div class="flex flex-wrap gap-2">
{#each availableArcs as arc (arc.id)}
<button
type="button"
onclick={() => toggleArcFilter(arc.id)}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.arcs.includes(
arc.id
)
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
>
{arc.name}
</button>
{/each}
</div>
</div>
<p class="mt-2 text-xs text-slate-500">
{characters.length} {characters.length > 1 ? $t.game.infinite.availableCharactersPlural : $t.game.infinite.availableCharactersSingular}
</p>
</div>
</div>
</section>
<!-- Column Visibility Toggle --> <!-- Column Visibility Toggle -->
<section class="mt-6"> <section class="mt-6">
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4"> <div class="rounded-2xl border border-white/10 bg-white/5 p-3 sm:p-4 backdrop-blur">
<div class="mb-3 flex items-center justify-between gap-3"> <div class="mb-3 flex items-center justify-between gap-3">
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase"> <h3 class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-200">Colonnes</h3>
{$t.game.infinite.columnsTitle}
</h3>
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys( {Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(columnVisibility).length}
columnVisibility
).length}
</p> </p>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">

View File

@@ -2,10 +2,7 @@ import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types'; import type { Actions } from './$types';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { auth } from '$lib/server/auth'; import { auth } from '$lib/server/auth';
import { db } from '$lib/server/db';
import { user } from '$lib/server/db/schema';
import { APIError } from 'better-auth/api'; import { APIError } from 'better-auth/api';
import { sql } from 'drizzle-orm';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
if (event.locals.user) { if (event.locals.user) {
@@ -17,28 +14,9 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = { export const actions: Actions = {
signInEmail: async (event) => { signInEmail: async (event) => {
const formData = await event.request.formData(); const formData = await event.request.formData();
const identifier = formData.get('identifier')?.toString().trim() ?? formData.get('email')?.toString().trim() ?? ''; const email = formData.get('email')?.toString() ?? '';
const password = formData.get('password')?.toString() ?? ''; const password = formData.get('password')?.toString() ?? '';
if (!identifier) {
return fail(400, { message: 'Email ou nom d\'utilisateur requis' });
}
let email = identifier;
if (!identifier.includes('@')) {
const [foundUser] = await db
.select({ email: user.email })
.from(user)
.where(sql`lower(${user.username}) = ${identifier.toLowerCase()}`)
.limit(1);
if (!foundUser) {
return fail(400, { message: 'Identifiants invalides' });
}
email = foundUser.email;
}
try { try {
await auth.api.signInEmail({ await auth.api.signInEmail({
body: { body: {
@@ -60,33 +38,7 @@ export const actions: Actions = {
const formData = await event.request.formData(); const formData = await event.request.formData();
const email = formData.get('email')?.toString() ?? ''; const email = formData.get('email')?.toString() ?? '';
const password = formData.get('password')?.toString() ?? ''; const password = formData.get('password')?.toString() ?? '';
const confirmPassword = formData.get('confirmPassword')?.toString() ?? '';
const name = formData.get('name')?.toString() ?? ''; const name = formData.get('name')?.toString() ?? '';
const username = formData.get('username')?.toString().trim() ?? '';
if (!username) {
return fail(400, { message: 'Nom d\'utilisateur requis' });
}
if (!/^[a-zA-Z0-9._-]{3,30}$/.test(username)) {
return fail(400, {
message: "Le nom d'utilisateur doit contenir 3 à 30 caractères (lettres, chiffres, ., _, -)"
});
}
const [existingUsername] = await db
.select({ id: user.id })
.from(user)
.where(sql`lower(${user.username}) = ${username.toLowerCase()}`)
.limit(1);
if (existingUsername) {
return fail(400, { message: "Ce nom d'utilisateur est déjà pris" });
}
if (password !== confirmPassword) {
return fail(400, { message: 'Les mots de passe ne correspondent pas' });
}
try { try {
await auth.api.signUpEmail({ await auth.api.signUpEmail({
@@ -94,7 +46,6 @@ export const actions: Actions = {
email, email,
password, password,
name, name,
username,
callbackURL: '/auth/verification-success' callbackURL: '/auth/verification-success'
} }
}); });

View File

@@ -1,14 +1,11 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { resolve } from '$app/paths';
import { t } from '$lib/i18n';
import type { ActionData } from './$types'; import type { ActionData } from './$types';
export let form: ActionData; export let form: ActionData;
let isSignUp = false; let isSignUp = false;
let name = ''; let name = '';
let username = '';
let email = ''; let email = '';
let password = ''; let password = '';
let confirmPassword = ''; let confirmPassword = '';
@@ -17,7 +14,6 @@
const handleToggle = () => { const handleToggle = () => {
isSignUp = !isSignUp; isSignUp = !isSignUp;
name = ''; name = '';
username = '';
email = ''; email = '';
password = ''; password = '';
confirmPassword = ''; confirmPassword = '';
@@ -26,11 +22,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>OnePieceDle - {isSignUp ? $t.game.login.titleSignUp : $t.game.login.titleSignIn}</title> <title>OnePieceDle - {isSignUp ? 'Inscription' : 'Connexion'}</title>
</svelte:head> </svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"> <main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100">
<div class="absolute inset-0 bg-linear-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 <div
class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]" 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>
@@ -43,7 +39,7 @@
OnePieceDle OnePieceDle
</h1> </h1>
<p class="mt-4 text-slate-300"> <p class="mt-4 text-slate-300">
{isSignUp ? $t.game.login.headerSignUp : $t.game.login.headerSignIn} {isSignUp ? 'Créer votre compte' : 'Bienvenue, pirate'}
</p> </p>
</div> </div>
@@ -65,7 +61,7 @@
{#if isSignUp} {#if isSignUp}
<div> <div>
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.login.nameLabel} Nom
</label> </label>
<input <input
id="name" id="name"
@@ -73,42 +69,24 @@
name="name" name="name"
bind:value={name} bind:value={name}
required required
placeholder={$t.game.login.namePlaceholder} placeholder="Votre nom"
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
{/if} {/if}
<!-- Username Field (Sign Up Only) --> <!-- Email Field -->
{#if isSignUp}
<div>
<label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.login.usernameLabel}
</label>
<input
id="username"
type="text"
name="username"
bind:value={username}
required
placeholder={$t.game.login.usernamePlaceholder}
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/>
</div>
{/if}
<!-- Email / Username Field -->
<div> <div>
<label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="email" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{isSignUp ? $t.game.login.identifierLabelSignUp : $t.game.login.identifierLabelSignIn} E-mail
</label> </label>
<input <input
id="identifier" id="email"
type={isSignUp ? 'email' : 'text'} type="email"
name={isSignUp ? 'email' : 'identifier'} name="email"
bind:value={email} bind:value={email}
required required
placeholder={isSignUp ? $t.game.login.identifierPlaceholderSignUp : $t.game.login.identifierPlaceholderSignIn} placeholder="votremail@email.com"
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -116,7 +94,7 @@
<!-- Password Field --> <!-- Password Field -->
<div> <div>
<label for="password" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="password" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.login.passwordLabel} Mot de passe
</label> </label>
<input <input
id="password" id="password"
@@ -136,7 +114,7 @@
for="confirmPassword" for="confirmPassword"
class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"
> >
{$t.game.login.confirmPasswordLabel} Confirmer le mot de passe
</label> </label>
<input <input
id="confirmPassword" id="confirmPassword"
@@ -163,20 +141,20 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? $t.game.login.loading : isSignUp ? $t.game.login.submitSignUp : $t.game.login.submitSignIn} {isLoading ? 'Chargement...' : isSignUp ? 'Créer un compte' : 'Se connecter'}
</button> </button>
</form> </form>
<!-- Toggle Sign Up / Login --> <!-- Toggle Sign Up / Login -->
<div class="mt-6 border-t border-white/10 pt-6"> <div class="mt-6 border-t border-white/10 pt-6">
<p class="text-center text-sm text-slate-400"> <p class="text-center text-sm text-slate-400">
{isSignUp ? $t.game.login.togglePromptSignUp : $t.game.login.togglePromptSignIn} {isSignUp ? 'Vous avez déjà un compte ?' : "Vous n'avez pas de compte ?"}
<button <button
type="button" type="button"
on:click={handleToggle} on:click={handleToggle}
class="text-amber-300 transition hover:text-amber-200" class="text-amber-300 transition hover:text-amber-200"
> >
{isSignUp ? $t.game.login.toggleActionSignUp : $t.game.login.toggleActionSignIn} {isSignUp ? 'Se connecter' : "S'inscrire"}
</button> </button>
</p> </p>
</div> </div>
@@ -184,8 +162,8 @@
<!-- Back to Home --> <!-- Back to Home -->
<div class="text-center"> <div class="text-center">
<a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300"> <a href="/" class="text-sm text-slate-400 transition hover:text-slate-300">
{$t.game.login.backHome} Retour à l'accueil
</a> </a>
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@ import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { auth } from '$lib/server/auth'; import { auth } from '$lib/server/auth';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema'; import { session } from '$lib/server/db/auth.schema';
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { APIError } from 'better-auth/api'; import { APIError } from 'better-auth/api';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
@@ -11,119 +11,15 @@ export const load: PageServerLoad = async (event) => {
return redirect(302, '/login'); return redirect(302, '/login');
} }
const currentUserId = event.locals.user.id;
// Fetch all sessions for this user // Fetch all sessions for this user
const userSessions = await db const userSessions = await db
.select() .select()
.from(session) .from(session)
.where(eq(session.userId, event.locals.user.id)); .where(eq(session.userId, event.locals.user.id));
// Fetch daily history for this user
const dailyHistoryRaw = await db
.select({
id: userCharacterHistory.id,
characterId: characterHistory.characterId,
date: characterHistory.date,
tryCount: userCharacterHistory.tryCount,
triedCharacterIds: userCharacterHistory.triedCharacterIds,
won: characterHistory.won,
characterName: character.name,
characterImage: character.pictureUrl
})
.from(userCharacterHistory)
.innerJoin(characterHistory, eq(userCharacterHistory.characterHistoryId, characterHistory.id))
.innerJoin(character, eq(characterHistory.characterId, character.id))
.where(eq(userCharacterHistory.userId, event.locals.user.id))
.orderBy(desc(characterHistory.date));
const uniqueTriedCharacterIds = Array.from(new Set(
dailyHistoryRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
));
const triedCharacters = uniqueTriedCharacterIds.length > 0
? await db
.select({
id: character.id,
name: character.name,
pictureUrl: character.pictureUrl
})
.from(character)
.where(inArray(character.id, uniqueTriedCharacterIds))
: [];
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
const dailyHistory = dailyHistoryRaw.map((entry) => ({
...entry,
triedCharacters: (entry.triedCharacterIds ?? [])
.map((characterId) => triedCharactersById.get(characterId))
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
}));
const incomingRequests = await db
.select({
id: friendship.id,
createdAt: friendship.createdAt,
requesterId: friendship.requesterId,
requesterName: user.name,
requesterUsername: user.username,
requesterImage: user.image
})
.from(friendship)
.innerJoin(user, eq(friendship.requesterId, user.id))
.where(and(eq(friendship.addresseeId, currentUserId), eq(friendship.status, 'pending')))
.orderBy(desc(friendship.createdAt));
const outgoingRequests = await db
.select({
id: friendship.id,
createdAt: friendship.createdAt,
addresseeId: friendship.addresseeId,
addresseeName: user.name,
addresseeUsername: user.username,
addresseeImage: user.image
})
.from(friendship)
.innerJoin(user, eq(friendship.addresseeId, user.id))
.where(and(eq(friendship.requesterId, currentUserId), eq(friendship.status, 'pending')))
.orderBy(desc(friendship.createdAt));
const acceptedAsRequester = await db
.select({
id: friendship.id,
createdAt: friendship.createdAt,
friendId: friendship.addresseeId,
friendName: user.name,
friendUsername: user.username,
friendImage: user.image
})
.from(friendship)
.innerJoin(user, eq(friendship.addresseeId, user.id))
.where(and(eq(friendship.requesterId, currentUserId), eq(friendship.status, 'accepted')));
const acceptedAsAddressee = await db
.select({
id: friendship.id,
createdAt: friendship.createdAt,
friendId: friendship.requesterId,
friendName: user.name,
friendUsername: user.username,
friendImage: user.image
})
.from(friendship)
.innerJoin(user, eq(friendship.requesterId, user.id))
.where(and(eq(friendship.addresseeId, currentUserId), eq(friendship.status, 'accepted')));
const friends = [...acceptedAsRequester, ...acceptedAsAddressee].sort((a, b) => b.createdAt - a.createdAt);
return { return {
user: event.locals.user, user: event.locals.user,
sessions: userSessions, sessions: userSessions
dailyHistory: dailyHistory,
incomingRequests,
outgoingRequests,
friends
}; };
}; };
@@ -215,214 +111,9 @@ export const actions: Actions = {
// Delete the session from database // Delete the session from database
await db.delete(session).where(eq(session.id, sessionId)); await db.delete(session).where(eq(session.id, sessionId));
} catch (error) { } catch (error) {
console.error('Error revoking session:', error);
return fail(500, { message: 'Erreur lors de la révocation de la session' }); return fail(500, { message: 'Erreur lors de la révocation de la session' });
} }
return { success: true, message: 'Session révoquée avec succès' }; return { success: true, message: 'Session révoquée avec succès' };
},
sendFriendRequest: async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
const formData = await event.request.formData();
const friendUsername = formData.get('friendUsername')?.toString().trim() ?? '';
if (!friendUsername) {
return fail(400, { message: 'Nom d\'utilisateur requis pour envoyer une demande' });
}
const me = event.locals.user;
const myUsername = (me as { username?: string }).username;
if (myUsername && friendUsername.toLowerCase() === myUsername.toLowerCase()) {
return fail(400, { message: 'Tu ne peux pas t\'ajouter toi-même' });
}
const [targetUser] = await db
.select({ id: user.id, username: user.username })
.from(user)
.where(sql`lower(${user.username}) = ${friendUsername.toLowerCase()}`)
.limit(1);
if (!targetUser) {
return fail(404, { message: 'Aucun utilisateur trouvé avec ce nom d\'utilisateur' });
}
const [existing] = await db
.select()
.from(friendship)
.where(
or(
and(eq(friendship.requesterId, me.id), eq(friendship.addresseeId, targetUser.id)),
and(eq(friendship.requesterId, targetUser.id), eq(friendship.addresseeId, me.id))
)
)
.limit(1);
const now = Date.now();
if (!existing) {
await db.insert(friendship).values({
requesterId: me.id,
addresseeId: targetUser.id,
status: 'pending',
createdAt: now,
updatedAt: now
});
return { success: true, message: 'Demande d\'ami envoyée' };
}
if (existing.status === 'accepted') {
return fail(400, { message: 'Vous êtes déjà amis' });
}
if (existing.status === 'pending') {
if (existing.requesterId === targetUser.id && existing.addresseeId === me.id) {
await db
.update(friendship)
.set({
status: 'accepted',
updatedAt: now
})
.where(eq(friendship.id, existing.id));
return { success: true, message: 'Demande acceptée automatiquement' };
}
return fail(400, { message: 'Demande déjà envoyée' });
}
await db
.update(friendship)
.set({
requesterId: me.id,
addresseeId: targetUser.id,
status: 'pending',
updatedAt: now
})
.where(eq(friendship.id, existing.id));
return { success: true, message: 'Demande d\'ami envoyée' };
},
acceptFriendRequest: async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
const formData = await event.request.formData();
const friendshipId = formData.get('friendshipId')?.toString() ?? '';
if (!friendshipId) {
return fail(400, { message: 'Demande invalide' });
}
const now = Date.now();
const result = await db
.update(friendship)
.set({ status: 'accepted', updatedAt: now })
.where(
and(
eq(friendship.id, friendshipId),
eq(friendship.addresseeId, event.locals.user.id),
eq(friendship.status, 'pending')
)
)
.returning({ id: friendship.id });
if (result.length === 0) {
return fail(404, { message: 'Demande introuvable' });
}
return { success: true, message: 'Demande acceptée' };
},
declineFriendRequest: async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
const formData = await event.request.formData();
const friendshipId = formData.get('friendshipId')?.toString() ?? '';
if (!friendshipId) {
return fail(400, { message: 'Demande invalide' });
}
const now = Date.now();
const result = await db
.update(friendship)
.set({ status: 'declined', updatedAt: now })
.where(
and(
eq(friendship.id, friendshipId),
eq(friendship.addresseeId, event.locals.user.id),
eq(friendship.status, 'pending')
)
)
.returning({ id: friendship.id });
if (result.length === 0) {
return fail(404, { message: 'Demande introuvable' });
}
return { success: true, message: 'Demande refusée' };
},
cancelFriendRequest: async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
const formData = await event.request.formData();
const friendshipId = formData.get('friendshipId')?.toString() ?? '';
if (!friendshipId) {
return fail(400, { message: 'Demande invalide' });
}
const result = await db
.delete(friendship)
.where(
and(
eq(friendship.id, friendshipId),
eq(friendship.requesterId, event.locals.user.id),
eq(friendship.status, 'pending')
)
)
.returning({ id: friendship.id });
if (result.length === 0) {
return fail(404, { message: 'Demande introuvable' });
}
return { success: true, message: 'Demande annulée' };
},
removeFriend: async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
const formData = await event.request.formData();
const friendshipId = formData.get('friendshipId')?.toString() ?? '';
if (!friendshipId) {
return fail(400, { message: 'Relation invalide' });
}
const result = await db
.delete(friendship)
.where(
and(
eq(friendship.id, friendshipId),
eq(friendship.status, 'accepted'),
or(eq(friendship.requesterId, event.locals.user.id), eq(friendship.addresseeId, event.locals.user.id))
)
)
.returning({ id: friendship.id });
if (result.length === 0) {
return fail(404, { message: 'Relation introuvable' });
}
return { success: true, message: 'Ami supprimé' };
} }
}; };

View File

@@ -1,56 +1,30 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { resolve } from '$app/paths';
import { t, language } from '$lib/i18n';
interface Props { interface Props {
data: PageData; data: PageData;
form?: { success?: boolean; message?: string } | null; form?: { success?: boolean; message?: string } | null;
} }
interface DailyHistoryEntry {
id: string;
characterId: string | null;
date: number;
tryCount: number;
won: number;
characterName: string;
characterImage: string | null;
triedCharacters?: Array<{
id: string;
name: string;
pictureUrl: string | null;
}>;
}
let { data, form }: Props = $props(); let { data, form }: Props = $props();
let isLoading = $state(false); let isLoading = $state(false);
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile'); let activeTab = $state<'profile' | 'password' | 'sessions'>('profile');
let name = $derived(data.user?.name || ''); let name = $state('');
let friendUsername = $state('');
let showSuccess = $state(false); let showSuccess = $state(false);
let oldPassword = $state(''); let oldPassword = $state('');
let newPassword = $state(''); let newPassword = $state('');
let confirmPassword = $state(''); let confirmPassword = $state('');
let sessions = $derived(data.sessions || []); let sessions = $state<any[]>([]);
let dailyHistory = $derived((data.dailyHistory || []) as DailyHistoryEntry[]);
let friends = $derived(data.friends || []);
let incomingRequests = $derived(data.incomingRequests || []);
let outgoingRequests = $derived(data.outgoingRequests || []);
let tabsElement: HTMLDivElement | undefined; let tabsElement: HTMLDivElement | undefined;
$effect(() => { $effect(() => {
friends = data.friends || []; name = data.user?.name || '';
}); });
$effect(() => { $effect(() => {
incomingRequests = data.incomingRequests || []; sessions = (data as any).sessions || [];
});
$effect(() => {
outgoingRequests = data.outgoingRequests || [];
}); });
$effect(() => { $effect(() => {
@@ -62,7 +36,7 @@
} }
}); });
const handleTabChange = (tab: 'profile' | 'password' | 'sessions' | 'daily' | 'friends') => { const handleTabChange = (tab: 'profile' | 'password' | 'sessions') => {
activeTab = tab; activeTab = tab;
}; };
@@ -72,11 +46,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t.game.profile.pageTitle} - OnePieceDle</title> <title>Mon Profil - OnePieceDle</title>
</svelte:head> </svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"> <main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100">
<div class="absolute inset-0 bg-linear-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 w-full max-w-2xl flex-col items-center px-6 py-4"> <div class="relative mx-auto flex w-full max-w-2xl flex-col items-center px-6 py-4">
@@ -84,10 +58,10 @@
<!-- Header --> <!-- Header -->
<div class="text-center"> <div class="text-center">
<h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl"> <h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl">
{$t.game.profile.headerTitle} Mon Profil
</h1> </h1>
<p class="mt-2 text-sm text-slate-300"> <p class="mt-2 text-sm text-slate-300">
{$t.game.profile.headerSubtitle} Modifie les informations de ton profil
</p> </p>
</div> </div>
@@ -95,43 +69,27 @@
<div bind:this={tabsElement} class="sticky top-20 z-10 flex gap-2 border-b border-white/10 bg-slate-950/80 backdrop-blur"> <div bind:this={tabsElement} class="sticky top-20 z-10 flex gap-2 border-b border-white/10 bg-slate-950/80 backdrop-blur">
<button <button
onclick={() => handleTabChange('profile')} onclick={() => handleTabChange('profile')}
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'profile' class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'profile'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
{$t.game.profile.tabProfile} Profil
</button> </button>
<button <button
onclick={() => handleTabChange('password')} onclick={() => handleTabChange('password')}
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'password' class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'password'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
{$t.game.profile.tabPassword} Mot de passe
</button>
<button
onclick={() => handleTabChange('daily')}
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'daily'
? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}"
>
{$t.game.profile.tabDaily}
</button> </button>
<button <button
onclick={() => handleTabChange('sessions')} onclick={() => handleTabChange('sessions')}
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'sessions' class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'sessions'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
{$t.game.profile.tabSessions} Sessions
</button>
<button
onclick={() => handleTabChange('friends')}
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'friends'
? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}"
>
{$t.game.profile.tabFriends}
</button> </button>
</div> </div>
@@ -143,7 +101,7 @@
{#if data.user.image} {#if data.user.image}
<img <img
src={data.user.image} src={data.user.image}
alt={data.user.name || $t.game.profile.avatarFallbackAlt} alt={data.user.name || 'Profil'}
class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover" class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover"
/> />
{:else} {:else}
@@ -152,7 +110,7 @@
</div> </div>
{/if} {/if}
<div class="text-center"> <div class="text-center">
<p class="text-sm text-slate-400">{$t.game.profile.email}</p> <p class="text-sm text-slate-400">Email</p>
<p class="font-semibold text-white">{data.user.email}</p> <p class="font-semibold text-white">{data.user.email}</p>
</div> </div>
</div> </div>
@@ -174,7 +132,7 @@
<!-- Name Field --> <!-- Name Field -->
<div> <div>
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.displayName} Nom d'affichage
</label> </label>
<input <input
id="name" id="name"
@@ -182,7 +140,7 @@
name="name" name="name"
bind:value={name} bind:value={name}
required required
placeholder={$t.game.profile.displayNamePlaceholder} placeholder="Ton nom"
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -197,7 +155,7 @@
<!-- Success Message --> <!-- Success Message -->
{#if showSuccess} {#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200"> <div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
{$t.game.profile.profileUpdateSuccess} Profil mis à jour avec succès !
</div> </div>
{/if} {/if}
@@ -207,139 +165,17 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? $t.game.profile.updating : $t.game.profile.saveChanges} {isLoading ? 'Mise à jour...' : 'Enregistrer les modifications'}
</button> </button>
</form> </form>
</div> </div>
{/if} {/if}
<!-- Friends Tab -->
{#if activeTab === 'friends'}
<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 sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
{$t.game.profile.friendsTitle}
</h2>
<form
method="POST"
action="?/sendFriendRequest"
use:enhance={() => {
isLoading = true;
return async ({ update }) => {
isLoading = false;
friendUsername = '';
await update();
};
}}
class="mb-8 space-y-3"
>
<label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.addFriendByUsername}
</label>
<div class="flex gap-2">
<input
id="friendUsername"
type="text"
name="friendUsername"
required
bind:value={friendUsername}
placeholder={$t.game.profile.friendUsernamePlaceholder}
class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/>
<button
type="submit"
disabled={isLoading}
class="rounded-full bg-amber-300 px-4 py-2 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
>
{isLoading ? $t.game.profile.sending : $t.game.profile.send}
</button>
</div>
{#if form?.message}
<p class="text-sm {form.success ? 'text-green-300' : 'text-red-300'}">{form.message}</p>
{/if}
</form>
<div class="space-y-8">
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.incomingRequests}</h3>
{#if incomingRequests.length === 0}
<p class="text-sm text-slate-400">{$t.game.profile.noIncomingRequests}</p>
{:else}
<div class="space-y-3">
{#each incomingRequests as req (req.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div>
<p class="font-semibold text-white">{req.requesterName}</p>
<p class="text-xs text-slate-400">@{req.requesterUsername}</p>
</div>
<div class="flex gap-2">
<form method="POST" action="?/acceptFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-emerald-400/50 bg-emerald-900/20 px-3 py-1.5 text-xs font-semibold text-emerald-300 transition hover:bg-emerald-900/40">{$t.game.profile.accept}</button>
</form>
<form method="POST" action="?/declineFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.decline}</button>
</form>
</div>
</div>
{/each}
</div>
{/if}
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.outgoingRequests}</h3>
{#if outgoingRequests.length === 0}
<p class="text-sm text-slate-400">{$t.game.profile.noOutgoingRequests}</p>
{:else}
<div class="space-y-3">
{#each outgoingRequests as req (req.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div>
<p class="font-semibold text-white">{req.addresseeName}</p>
<p class="text-xs text-slate-400">@{req.addresseeUsername}</p>
</div>
<form method="POST" action="?/cancelFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.cancel}</button>
</form>
</div>
{/each}
</div>
{/if}
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.myFriends}</h3>
{#if friends.length === 0}
<p class="text-sm text-slate-400">{$t.game.profile.noFriends}</p>
{:else}
<div class="space-y-3">
{#each friends as friend (friend.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div>
<p class="font-semibold text-white">{friend.friendName}</p>
<p class="text-xs text-slate-400">@{friend.friendUsername}</p>
</div>
<form method="POST" action="?/removeFriend" use:enhance>
<input type="hidden" name="friendshipId" value={friend.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.remove}</button>
</form>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}
<!-- Password Tab --> <!-- Password Tab -->
{#if activeTab === 'password'} {#if activeTab === 'password'}
<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 sm:p-8"> <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 sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
{$t.game.profile.changePasswordTitle} Changer le mot de passe
</h2> </h2>
<!-- Form --> <!-- Form -->
@@ -361,7 +197,7 @@
<!-- Old Password Field --> <!-- Old Password Field -->
<div> <div>
<label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.currentPassword} Mot de passe actuel
</label> </label>
<input <input
id="oldPassword" id="oldPassword"
@@ -377,7 +213,7 @@
<!-- New Password Field --> <!-- New Password Field -->
<div> <div>
<label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.newPassword} Nouveau mot de passe
</label> </label>
<input <input
id="newPassword" id="newPassword"
@@ -393,7 +229,7 @@
<!-- Confirm Password Field --> <!-- Confirm Password Field -->
<div> <div>
<label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.confirmPassword} Confirmer le mot de passe
</label> </label>
<input <input
id="confirmPassword" id="confirmPassword"
@@ -416,7 +252,7 @@
<!-- Success Message --> <!-- Success Message -->
{#if showSuccess} {#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200"> <div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
{$t.game.profile.passwordChangeSuccess} Mot de passe changé avec succès !
</div> </div>
{/if} {/if}
@@ -426,110 +262,34 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? $t.game.profile.changing : $t.game.profile.changePassword} {isLoading ? 'Changement en cours...' : 'Changer le mot de passe'}
</button> </button>
</form> </form>
</div> </div>
{/if} {/if}
<!-- Daily History Tab -->
{#if activeTab === 'daily'}
<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 sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
{$t.game.profile.dailyHistoryTitle}
</h2>
{#if dailyHistory.length === 0}
<p class="text-center text-slate-400">{$t.game.profile.noDailyHistory}</p>
{:else}
<div class="space-y-4">
{#each dailyHistory as day (day.id)}
<div class="flex items-center gap-4 rounded-lg border border-white/10 bg-white/5 p-4">
<!-- Character Image -->
<div class="shrink-0">
{#if day.characterImage}
<img
src={day.characterImage}
alt={day.characterName}
class="h-16 w-16 rounded-lg border border-white/20 object-cover"
/>
{:else}
<div class="flex h-16 w-16 items-center justify-center rounded-lg border border-white/20 bg-slate-700">
<span class="text-xs text-slate-400">{$t.game.profile.noImage}</span>
</div>
{/if}
</div>
<!-- Character Info -->
<div class="flex-1">
<p class="font-semibold text-white">{day.characterName}</p>
<p class="text-xs text-slate-400">
{new Date(day.date).toLocaleDateString($language === 'fr' ? 'fr-FR' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
<div class="mt-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
{$t.game.profile.triedCharactersTitle}
</p>
{#if day.triedCharacters && day.triedCharacters.length > 0}
<div class="mt-2 flex flex-wrap gap-2">
{#each day.triedCharacters as triedCharacter (triedCharacter.id)}
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
{#if triedCharacter.pictureUrl}
<img
src={triedCharacter.pictureUrl}
alt={triedCharacter.name}
class="h-4 w-4 rounded-full object-cover"
/>
{/if}
{triedCharacter.name}
</span>
{/each}
</div>
{:else}
<p class="mt-1 text-xs text-slate-500">{$t.game.profile.noTriedCharacters}</p>
{/if}
</div>
</div>
<!-- Tries -->
<div class="flex flex-col items-end">
<p class="text-xs text-slate-400">
{day.tryCount} {day.tryCount === 1 ? $t.game.profile.trySingular : $t.game.profile.tryPlural}
</p>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}
<!-- Sessions Tab --> <!-- Sessions Tab -->
{#if activeTab === 'sessions'} {#if activeTab === 'sessions'}
<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 sm:p-8"> <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 sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
{$t.game.profile.activeSessionsTitle} Sessions actives
</h2> </h2>
{#if sessions.length === 0} {#if sessions.length === 0}
<p class="text-center text-slate-400">{$t.game.profile.noActiveSessions}</p> <p class="text-center text-slate-400">Aucune session active</p>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each sessions as sess (sess.id)} {#each sessions as sess}
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4"> <div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4">
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold text-white"> <p class="font-semibold text-white">
{sess.userAgent || $t.game.profile.unknownDevice} {sess.userAgent || 'Appareil inconnu'}
</p> </p>
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
{$t.game.profile.ip}: {sess.ipAddress || $t.game.profile.unknown} IP: {sess.ipAddress || 'Inconnue'}
</p> </p>
<p class="mt-1 text-xs text-slate-500"> <p class="mt-1 text-xs text-slate-500">
{$t.game.profile.created}: {new Date(sess.createdAt).toLocaleDateString($language === 'fr' ? 'fr-FR' : 'en-US', { Créée: {new Date(sess.createdAt).toLocaleDateString('fr-FR', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@@ -553,7 +313,7 @@
type="submit" type="submit"
class="rounded-lg border border-red-500/50 bg-red-900/20 px-4 py-2 text-xs font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/40" class="rounded-lg border border-red-500/50 bg-red-900/20 px-4 py-2 text-xs font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/40"
> >
{$t.game.profile.terminate} Terminer
</button> </button>
</form> </form>
</div> </div>
@@ -565,8 +325,8 @@
<!-- Back to Home --> <!-- Back to Home -->
<div class="text-center"> <div class="text-center">
<a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300"> <a href="/" class="text-sm text-slate-400 transition hover:text-slate-300">
{$t.game.profile.backHome} Retour à l'accueil
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import './layout.css'; import './layout.css';
import favicon from '$lib/assets/favicon.png'; import favicon from '$lib/assets/favicon.svg';
let { children } = $props(); let { children } = $props();
</script> </script>

View File

@@ -1,52 +0,0 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import type { RequestHandler } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
export const GET: RequestHandler = async ({ params }) => {
const filename = params.filename as string;
if (!filename || typeof filename !== 'string' || filename.includes('..') || filename.includes('/')) {
return new Response('Invalid filename', { status: 400 });
}
try {
const uploadsDir = env.UPLOADS_DIR || join(process.cwd(),'uploads');
const filepath = join(uploadsDir, filename);
if (!existsSync(filepath)) {
return new Response('File not found', { status: 404 });
}
// Verify the file is within uploads directory (prevent directory traversal)
if (!filepath.startsWith(uploadsDir)) {
return new Response('Access denied', { status: 403 });
}
const fileBuffer = await readFile(filepath);
// Determine content type based on file extension
const ext = (filename as string).split('.').pop()?.toLowerCase() || '';
const contentTypes: Record<string, string> = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml'
};
const contentType = contentTypes[ext] || 'application/octet-stream';
return new Response(fileBuffer, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400'
}
});
} catch (error) {
console.error('File serving error:', error);
return new Response('Internal server error', { status: 500 });
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB