Compare commits

..

37 Commits

Author SHA1 Message Date
ef6bf9862e feat: add FriendsTodaySection component for displaying friends' results
All checks were successful
Build Docker Image / build (push) Successful in 2m4s
2026-04-14 22:08:49 +02:00
d75c74ac3c Refactor character affiliations to singular form
- Updated character data structure to replace 'affiliations' and 'frAffiliations' with 'affiliation' and 'frAffiliation'.
- Modified related functions and components to accommodate the new structure.
- Adjusted database schema and server-side logic to reflect the changes in character affiliation handling.
- Ensured all references in the UI components and data import/export scripts are updated accordingly.
2026-04-14 21:56:26 +02:00
fa14156d82 feat: remove overrides
Some checks failed
Build Docker Image / build (push) Has been cancelled
2026-04-12 02:01:01 +02:00
29297d3773 fix: include 'Four Emperors' in character exclusion logic
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-18 22:42:32 +01:00
28bb8f526b fix: include Kuja in female character categorization
All checks were successful
Build Docker Image / build (push) Successful in 1m13s
2026-03-18 22:40:39 +01:00
288271fb04 fix: include Queens Regnant in female character categorization
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-18 22:22:54 +01:00
fb64c84a17 fix: include Gorgon Sisters in female character categorization
All checks were successful
Build Docker Image / build (push) Successful in 1m11s
2026-03-18 22:19:46 +01:00
81e205dd4e fix: expand gender categorization in character fetch logic
All checks were successful
Build Docker Image / build (push) Successful in 1m23s
2026-03-18 22:07:28 +01:00
ded1c8313d fix: update character link URLs to remove language prefix
All checks were successful
Build Docker Image / build (push) Successful in 1m11s
2026-03-16 23:15:02 +01:00
4426b5d28a fix: correct href interpolation for character links in admin page
Some checks failed
Build Docker Image / build (push) Has been cancelled
2026-03-16 23:14:21 +01:00
5ad0428420 feat: enhance character scrape validation and management
All checks were successful
Build Docker Image / build (push) Successful in 1m14s
- Added a new entry for "fuzzy_talisman" in the journal.
- Updated import-json script to handle character deletion and mark absent characters as deleted in the scrape validation.
- Modified schema to include an `isDeleted` field in the characterScrapeValidation table.
- Renamed function `upsertCharacterFromScrapeValidation` to `applyCharacterChangeFromScrapeValidation` for clarity.
- Enhanced character change loading to include deleted characters and updated UI to display them.
- Improved character change handling in the Svelte component to reflect new, modified, and deleted states.
2026-03-16 23:12:06 +01:00
7760570365 feat: exclude characters with 'family' in their name from fetch results
All checks were successful
Build Docker Image / build (push) Successful in 1m14s
2026-03-16 22:31:40 +01:00
5fde54a2a7 feat: add age filter functionality and localization support in guess history
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-16 22:05:09 +01:00
2a3c82f777 feat: add age attribute to character history and localization support
All checks were successful
Build Docker Image / build (push) Successful in 1m13s
2026-03-16 22:00:49 +01:00
835163f5bb feat: add tried characters tracking and display in daily game profile
All checks were successful
Build Docker Image / build (push) Successful in 1m10s
2026-03-16 21:39:44 +01:00
5020393b22 fix: normalize character status text to lowercase for consistency
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
2026-03-16 21:17:01 +01:00
94393851c8 feat: support French epithets in character extraction logic
All checks were successful
Build Docker Image / build (push) Successful in 1m14s
2026-03-15 22:44:45 +01:00
997b2f1781 feat: add French localization support for character attributes and improve character display logic
All checks were successful
Build Docker Image / build (push) Successful in 1m18s
- Added optional French names, affiliations, origins, and epithets to character records.
- Updated character import logic to handle new French fields.
- Enhanced character search and display components to show French names and epithets based on selected language.
- Modified database schema to include French fields for characters.
- Improved error handling in daily character setup to check for existing characters.
- Refactored components to utilize helper functions for displaying names and attributes based on language.
2026-03-15 22:00:19 +01:00
bd121b7d85 feat(i18n): integrate internationalization for game pages
- Added translation support for various game-related texts in the home, daily, infinite, login, and profile pages.
- Replaced hardcoded French strings with translation keys using the `$t` function.
- Updated titles, descriptions, and button texts to enhance localization.
2026-03-15 20:19:26 +01:00
6d2dccd47f refactor: update link generation to use resolve for consistent path handling 2026-03-15 19:53:47 +01:00
5fdde9d177 refactor: remove unused fandomUrl and getDifferenceColor functions, simplify character link generation 2026-03-14 18:34:03 +01:00
b1cc691422 refactor: enhance character data transformation and improve fetching logic in character-related scripts 2026-03-14 18:32:43 +01:00
8b08950719 refactor: streamline character selection and improve rendering logic in +page.svelte 2026-03-14 17:29:57 +01:00
fd83ac911a refactor: improve type definitions for selectedCharacter and selectedCharacters in WinPanel component 2026-03-14 17:22:44 +01:00
eeccf812cf refactor: improve type definitions for dailyCharacter and selectedCharacters in HintsPanel component 2026-03-14 17:20:26 +01:00
9485d9841c refactor: improve type definitions and event handling in CharacterSearchInput component 2026-03-14 17:18:58 +01:00
31308ef126 refactor: enhance affiliation handling and improve type definitions in GuessHistoryTable component 2026-03-14 17:13:13 +01:00
57a0427e77 refactor: improve bounty extraction logic and enhance character selection in infinite mode 2026-03-14 17:09:33 +01:00
3bd2506c2f refactor: enhance type safety and improve layout structure in GuessHistoryTable component 2026-03-14 16:33:43 +01:00
e5a21cb0af refactor: improve type definitions and enhance state management in profile and daily components 2026-03-14 16:31:16 +01:00
4e95abf09f fix: update devil fruit extraction to handle redirects and ensure correct title retrieval
Some checks failed
Build Docker Image / build (push) Has been cancelled
2026-03-14 15:41:20 +01:00
66afda5101 Refactor code structure for improved readability and maintainability 2026-03-14 15:34:30 +01:00
a041a8caf5 Refactor database schema and update scraping logic for One Piece characters and arcs
- Updated database schema to include French names and adjusted field names for consistency.
- Modified scraping script to fetch and store French names for arcs and characters.
- Improved API calls to handle redirects and fetch additional data for characters.
- Enhanced data extraction methods for character attributes and devil fruits.
- Cleaned up code for better readability and maintainability.
2026-03-14 01:23:29 +01:00
a91b298ee5 fix: unify fetch concurrency constant for character and devil fruit fetching
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-06 21:57:09 +01:00
d3e36e486f feat: display GuessHistoryTable after winning condition and friends' results
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-06 20:26:04 +01:00
249da5ad2e feat(auth): add username field to user schema and authentication process
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
- Updated user schema to include a unique username field.
- Modified authentication logic to support sign-in using either email or username.
- Enhanced sign-up process to require a username and validate its uniqueness.
- Updated login and profile routes to reflect changes in user identification.
- Adjusted frontend forms to accommodate username input alongside email.
2026-03-06 20:16:05 +01:00
ce08329b2d feat: add friends' results display for today's game
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
2026-03-06 19:34:46 +01:00
63 changed files with 5552 additions and 9104 deletions

View File

@@ -1,156 +0,0 @@
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

@@ -0,0 +1,199 @@
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

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

View File

@@ -1,30 +0,0 @@
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

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

View File

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

View File

@@ -0,0 +1,8 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,9 +0,0 @@
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,15 +0,0 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_userCharacterHistory` (
`id` text PRIMARY KEY NOT NULL,
`userId` text,
`characterHistoryId` 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 (`characterHistoryId`) REFERENCES `characterHistory`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
INSERT INTO `__new_userCharacterHistory`("id", "userId", "characterHistoryId", "tryCount", "createdAt") SELECT "id", "userId", "characterId", "tryCount", "createdAt" FROM `userCharacterHistory`;--> statement-breakpoint
DROP TABLE `userCharacterHistory`;--> statement-breakpoint
ALTER TABLE `__new_userCharacterHistory` RENAME TO `userCharacterHistory`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -1 +0,0 @@
CREATE UNIQUE INDEX `userCharacterHistory_userId_characterHistoryId_unique` ON `userCharacterHistory` (`userId`,`characterHistoryId`);

View File

@@ -1,29 +0,0 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_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 false,
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_character`("id", "name", "gender", "age", "affiliations", "devilFruitId", "hakiObservation", "hakiArmament", "hakiConqueror", "bounty", "height", "origin", "firstAppearance", "pictureUrl", "epithets", "status", "arcId", "url", "isInDailyMode") SELECT "id", "name", "gender", "age", "affiliations", "devilFruitId", "hakiObservation", "hakiArmament", "hakiConqueror", "bounty", "height", "origin", "firstAppearance", "pictureUrl", "epithets", "status", "arcId", "url", "isInDailyMode" FROM `character`;--> statement-breakpoint
DROP TABLE `character`;--> statement-breakpoint
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -1,12 +0,0 @@
CREATE TABLE `friendship` (
`id` text PRIMARY KEY NOT NULL,
`requesterId` text NOT NULL,
`addresseeId` text NOT NULL,
`status` text DEFAULT 'pending' NOT NULL,
`createdAt` integer NOT NULL,
`updatedAt` integer NOT NULL,
FOREIGN KEY (`requesterId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`addresseeId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `friendship_requesterId_addresseeId_unique` ON `friendship` (`requesterId`,`addresseeId`);

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,64 +5,29 @@
{
"idx": 0,
"version": "6",
"when": 1772325597983,
"tag": "0000_graceful_master_mold",
"when": 1773602933375,
"tag": "0000_huge_doctor_octopus",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1772383366179,
"tag": "0001_nostalgic_hercules",
"when": 1773697753818,
"tag": "0001_fuzzy_talisman",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1772390182445,
"tag": "0002_large_gwen_stacy",
"when": 1775950314114,
"tag": "0002_old_earthquake",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1772449624450,
"tag": "0003_wise_blonde_phantom",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1772480377099,
"tag": "0004_unique_lorna_dane",
"breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1772562012631,
"tag": "0005_large_jane_foster",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1772562364830,
"tag": "0006_premium_mesmero",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1772735982970,
"tag": "0007_gray_shinko_yamashiro",
"breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1772821532270,
"tag": "0008_skinny_warpath",
"when": 1776195681488,
"tag": "0003_mixed_ben_grimm",
"breakpoints": true
}
]

View File

@@ -1,144 +1,255 @@
[
"aladdin_aladdin",
"alvida_alvida",
"aramaki_aramaki",
"arlong_arlong",
"ashura_doji_ashura_doji",
"baby_5_baby_5",
"baggy_baggy",
"bartholomew_kuma_bartholomew_kuma",
"bartolomeo_bartolomeo",
"basil_hawkins_basil_hawkins",
"batman_batman",
"bellamy_bellamy",
"belo_betty_belo_betty",
"ben_beckman_ben_beckman",
"bentham_bentham",
"bepo_bepo",
"black_maria_black_maria",
"boa_hancock_boa_hancock",
"boa_marigold_boa_marigold",
"boa_sandersonia_boa_sandersonia",
"borsalino_borsalino",
"brogy_brogy",
"brook_brook",
"camie_camie",
"capone_bege_capone_bege",
"caribou_caribou",
"carrot_carrot",
"catarina_devon_catarina_devon",
"cavendish_cavendish",
"cesar_clown_cesar_clown",
"chinjao_chinjao",
"coby_coby",
"corazon_corazon",
"crocodile_crocodile",
"crocus_crocus",
"curly_dadan_curly_dadan",
"dalton_dalton",
"daz_bones_daz_bones",
"denjiro_denjiro",
"diamante_diamante",
"doc_q_doc_q",
"don_quichotte_doflamingo_don_quichotte_doflamingo",
"don_quichotte_rossinante_don_quichotte_rossinante",
"dorry_dorry",
"dracule_mihawk_dracule_mihawk",
"duval_duval",
"edward_newgate_edward_newgate",
"edward_weevil_edward_weevil",
"emporio_ivankov_emporio_ivankov",
"enel_enel",
"eustass_kid_eustass_kid",
"fisher_tiger_fisher_tiger",
"foxy_foxy",
"franky_franky",
"fujitora_fujitora",
"gan_forr_gan_forr",
"gecko_moria_gecko_moria",
"gin_gin",
"gol_d_roger_gol_d_roger",
"haguar_d_sauro_haguar_d_sauro",
"hajrudin_hajrudin",
"hannyabal_hannyabal",
"hatchan_hatchan",
"hina_hina",
"hody_jones_hody_jones",
"hyogoro_hyogoro",
"iceburg_iceburg",
"imu_imu",
"inazuma_inazuma",
"inuarashi_inuarashi",
"issho_issho",
"izo_izo",
"jabra_jabra",
"jack_jack",
"jesus_burgess_jesus_burgess",
"jewelry_bonney_jewelry_bonney",
"jinbei_jinbei",
"joy_boy_joy_boy",
"kaidou_kaidou",
"kaku_kaku",
"kalgara_kalgara",
"kalifa_kalifa",
"karasu_karasu",
"karoo_karoo",
"kawamatsu_kawamatsu",
"kaya_kaya",
"killer_killer",
"kinemon_kinemon",
"koala_koala",
"koby_koby",
"kong_kong",
"kozuki_hiyori_kozuki_hiyori",
"kozuki_momonosuke_kozuki_momonosuke",
"kozuki_oden_kozuki_oden",
"krieg_krieg",
"kureha_kureha",
"kuro_kuro",
"kurozumi_orochi_kurozumi_orochi",
"kuzan_kuzan",
"kyros_kyros",
"laboon_laboon",
"laffitte_laffitte",
"lao_g_lao_g",
"leo_leo",
"lindbergh_lindbergh",
"loki_loki",
"lucky_roux_lucky_roux",
"magellan_magellan",
"makino_makino",
"marco_marco",
"marshall_d_teach_marshall_d_teach",
"monkey_d_dragon_monkey_d_dragon",
"monkey_d_garp_monkey_d_garp",
"monkey_d_luffy_monkey_d_luffy",
"montblanc_norland_montblanc_norland",
"morgans_morgans",
"morley_morley",
"mr_3_mr_3",
"nami_nami",
"nefertari_cobra_nefertari_cobra",
"nefertari_vivi_nefertari_vivi",
"nekomamushi_nekomamushi",
"neptune_neptune",
"nico_robin_nico_robin",
"oars_oars",
"otohime_otohime",
"page_one_page_one",
"pandaman_pandaman",
"pekoms_pekoms",
"pell_pell",
"perona_perona",
"pica_pica",
"portgas_d_ace_portgas_d_ace",
"queen_queen",
"raizo_raizo",
"rebecca_rebecca",
"rob_lucci_rob_lucci",
"rocks_d_xebec_rocks_d_xebec",
"roronoa_zoro_roronoa_zoro",
"sabo_sabo",
"vegapunk_vegapunk",
"yamato_yamato"
"absalom_absalom",
"king_king",
"alvida_alvida",
"aramaki_aramaki",
"arlong_arlong",
"ashura_doji_ashura_doji",
"vegapunk/atlas_atlas",
"avalo_pizarro_avalo_pizarro",
"baby_5_baby_5",
"buggy_buggy",
"bartholomew_kuma_bartholomew_kuma",
"bartolomeo_bartolomeo",
"basil_hawkins_basil_hawkins",
"bell-mère_bell-mère",
"bellamy_bellamy",
"belo_betty_belo_betty",
"benn_beckman_ben_beckman",
"bentham_bentham",
"bepo_bepo",
"black_maria_black_maria",
"blueno_blueno",
"boa_hancock_boa_hancock",
"boa_marigold_boa_marigold",
"boa_sandersonia_boa_sandersonia",
"borsalino_borsalino",
"brogy_brogy",
"brook_brook",
"buckingham_stussy_buckingham_stussy",
"buffalo_buffalo",
"camie_camie",
"capone_bege_capone_bege",
"carmel_carmel",
"caribou_caribou",
"carrot_carrot",
"catarina_devon_catarina_devon",
"cavendish_cavendish",
"caesar_clown_caesar_clown",
"charlotte_brûlée_charlotte_brûlée",
"charlotte_cracker_charlotte_cracker",
"charlotte_katakuri_charlotte_katakuri",
"charlotte_linlin_charlotte_linlin",
"charlotte_mont-d'or_charlotte_mont-d'or",
"charlotte_oven_charlotte_oven",
"charlotte_perospero_charlotte_perospero",
"charlotte_pudding_charlotte_pudding",
"charlotte_smoothie_charlotte_smoothie",
"chinjao_chinjao",
"clou_d_clover_clou_d_clover",
"crocodile_crocodile",
"crocus_crocus",
"curly_dadan_curly_dadan",
"dalton_dalton",
"daz_bonez_daz_bonez",
"denjiro_denjiro",
"diamante_diamante",
"doc_q_doc_q",
"donquixote_doflamingo_donquixote_doflamingo",
"donquixote_rosinante_donquixote_rosinante",
"dorry_dorry",
"dracule_mihawk_dracule_mihawk",
"vegapunk/edison_edison",
"edward_newgate_edward_newgate",
"edward_weevil_edward_weevil",
"emporio_ivankov_emporio_ivankov",
"enel_enel",
"eustass_kid_eustass_kid",
"fisher_tiger_fisher_tiger",
"foxy_foxy",
"franky_franky",
"fukaboshi_fukaboshi",
"fukurou_fukurou",
"galdino_galdino",
"gan_fall_gan_fall",
"gecko_moria_gecko_moria",
"gem_gem",
"genzo_genzo",
"gin_gin",
"ginny_ginny",
"gol_d_roger_gol_d_roger",
"guernika_guernika",
"hack_hack",
"jaguar_d_saul_jaguar_d_saul",
"hajrudin_hajrudin",
"hannyabal_hannyabal",
"harald_harald",
"haredas_haredas",
"heracles_heracles",
"helmeppo_helmeppo",
"hibari_hibari",
"hiriluk_hiriluk",
"hina_hina",
"hody_jones_hody_jones",
"hogback_hogback",
"hyougoro_hyougoro",
"iceburg_iceburg",
"igaram_igaram",
"imu_imu",
"inazuma_inazuma",
"inuarashi_inuarashi",
"issho_issho",
"izou_izou",
"jabra_jabra",
"jack_jack",
"jango_jango",
"jesus_burgess_jesus_burgess",
"jewelry_bonney_jewelry_bonney",
"jinbe_jinbe",
"giolla_giolla",
"joy_boy_joy_boy",
"jozu_jozu",
"kaidou_kaidou",
"kaku_kaku",
"kalgara_kalgara",
"kalifa_kalifa",
"karasu_karasu",
"karoo_karoo",
"kawamatsu_kawamatsu",
"kaya_kaya",
"kelly_funk_kelly_funk",
"kikunojo_kikunojo",
"killer_killer",
"kin'emon_kin'emon",
"koala_koala",
"koby_koby",
"kokoro_kokoro",
"kouzuki_hiyori_kouzuki_hiyori",
"kouzuki_momonosuke_kouzuki_momonosuke",
"kouzuki_oden_kouzuki_oden",
"kouzuki_sukiyaki_kouzuki_sukiyaki",
"kouzuki_toki_kouzuki_toki",
"krieg_krieg",
"kumadori_kumadori",
"kureha_kureha",
"kuro_kuro",
"kurozumi_kanjuro_kurozumi_kanjuro",
"kurozumi_orochi_kurozumi_orochi",
"kurozumi_tama_kurozumi_tama",
"kuzan_kuzan",
"kyros_kyros",
"laboon_laboon",
"laffitte_laffitte",
"lao_g_lao_g",
"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,12 +1,15 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq } from 'drizzle-orm';
import { sql, eq, inArray } from 'drizzle-orm';
import fs from 'fs';
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
type Status = 'Alive' | 'Dead' | 'Unknown';
type ArcRecord = {
id: string;
name: string;
frName?: string | null;
startChapter: number;
endChapter?: number | null;
url?: string | null;
@@ -22,9 +25,11 @@ type DevilFruitRecord = {
type CharacterRecord = {
id: string;
name: string;
frName?: string | null;
gender?: string | null;
age?: number | null;
affiliations?: string[] | string | null;
affiliation?: string | null;
frAffiliation?: string | null;
devilFruitId?: string | null;
hakiObservation?: boolean;
hakiArmament?: boolean;
@@ -32,12 +37,15 @@ type CharacterRecord = {
bounty?: number | null;
height?: number | null;
origin?: string | null;
frOrigin?: string | null;
firstAppearance?: number;
pictureUrl?: string | null;
epithets?: string[] | string | null;
status?: string | null;
frEpithets?: string[] | string | null;
status?: Status | null;
arcId?: string | null;
url?: string | null;
frUrl?: string | null;
};
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
@@ -86,7 +94,7 @@ function toJsonArray(value: string[] | string | null | undefined): string[] | nu
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
if (!value) return null;
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Smile' || value === 'Unknown') {
return value;
}
return 'Unknown';
@@ -112,62 +120,31 @@ function transformCharacterData(item: CharacterRecord) {
return {
id: item.id,
name: item.name,
frName: toNullable(item.frName),
gender: toNullable(item.gender),
age: toNullable(item.age),
affiliations: toJsonArray(item.affiliations),
affiliation: toNullable(item.affiliation),
frAffiliation: toNullable(item.frAffiliation),
devilFruitId: toNullable(item.devilFruitId),
hakiObservation: !!item.hakiObservation,
hakiArmament: !!item.hakiArmament,
hakiConqueror: !!item.hakiConqueror,
bounty: item.bounty ?? 0,
height: toNumber(item.height as any),
height: toNumber(item.height as string | number | null),
origin: toNullable(item.origin),
frOrigin: toNullable(item.frOrigin),
firstAppearance: item.firstAppearance ?? 0,
pictureUrl: toNullable(item.pictureUrl),
epithets: toJsonArray(item.epithets),
frEpithets: toJsonArray(item.frEpithets),
status: toNullable(item.status),
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> {
const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character);
return result[0]?.count === 0;
@@ -195,6 +172,7 @@ async function importFromJson(): Promise<void> {
.values({
id: item.id,
name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
@@ -203,6 +181,7 @@ async function importFromJson(): Promise<void> {
target: arc.id,
set: {
name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
@@ -329,6 +308,7 @@ async function importFromJson(): Promise<void> {
} else {
// Update scrapeValidation table
console.log('Characters table not empty, updating scrapeValidation table for changes...\n');
const scrapedCharacterIds: string[] = [];
for (let i = 0; i < characters.length; i++) {
const item = characters[i];
@@ -341,6 +321,7 @@ async function importFromJson(): Promise<void> {
lastSql = selectQuery.toSQL();
scrapedCharacterIds.push(item.id);
const jsonData = transformCharacterData(item);
const upsertQuery = db
@@ -363,6 +344,57 @@ async function importFromJson(): Promise<void> {
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!`);

View File

@@ -23,7 +23,8 @@ const columns = [
'origin',
'devilFruitType',
'arc',
'status'
'status',
'age'
] as const;
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 { drizzle } from 'drizzle-orm/libsql';
import { eq } from 'drizzle-orm';
import { eq, inArray } from 'drizzle-orm';
import fs from 'fs';
import { character, characterHistory } from '../src/lib/server/db/schema';
@@ -24,13 +24,14 @@ function getErrorMessage(error: unknown): string {
async function setDailyCharacters(): Promise<void> {
try {
const dailyCharacterIds = readJsonFile('./scripts/daily-characters.json');
const dailyCharacterIdsRaw = readJsonFile('./scripts/daily-characters.json');
if (!dailyCharacterIds || dailyCharacterIds.length === 0) {
console.error('No daily characters found in daily-characters.json');
process.exit(1);
if (!dailyCharacterIdsRaw || dailyCharacterIdsRaw.length === 0) {
throw new Error('No daily characters found in daily-characters.json');
}
const dailyCharacterIds = dailyCharacterIdsRaw;
console.log(`\n=== Setting Daily Mode Characters ===\n`);
console.log(`Found ${dailyCharacterIds.length} characters to set as daily\n`);
@@ -45,16 +46,36 @@ async function setDailyCharacters(): Promise<void> {
let successCount = 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++) {
const charId = dailyCharacterIds[i];
if (!existingIdSet.has(charId)) {
continue;
}
try {
const result = await db
await db
.update(character)
.set({ isInDailyMode: true })
.where(eq(character.id, charId));
successCount++;
process.stdout.write(`\rUpdated: ${successCount}/${dailyCharacterIds.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error updating character ${i + 1}:`);

View File

@@ -1,16 +1,64 @@
<script lang="ts">
import type { CharacterWithRelations } from '$lib/server/daily-character';
import { onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { language, t } from '$lib/i18n';
export let characters: any[];
export let selectedCharacters: any[];
let {
characters,
selectedCharacters,
onSelect
}: {
characters: CharacterWithRelations[];
selectedCharacters: CharacterWithRelations[];
onSelect: (character: CharacterWithRelations) => void;
} = $props();
const dispatch = createEventDispatcher();
const state = $state({
searchInput: '',
highlightedIndex: 0,
dropdownContainer: null as HTMLDivElement | null,
searchContainer: null as HTMLDivElement | null
});
let searchInput = '';
let highlightedIndex = 0;
let dropdownContainer: HTMLDivElement;
let searchContainer: HTMLDivElement;
const isFrench = $derived($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 normalizeSearchText(value: string): string {
return value
@@ -28,52 +76,54 @@
};
});
$: filteredCharacters = characters.filter(char => {
const searchTerm = normalizeSearchText(searchInput);
const nameMatches = normalizeSearchText(char.name).includes(searchTerm);
const filteredCharacters = $derived.by(() => {
const searchTerm = normalizeSearchText(state.searchInput);
let epithetsMatches = false;
if (char.epithets) {
try {
const parsedEpithets = typeof char.epithets === 'string'
? JSON.parse(char.epithets)
: char.epithets;
return characters.filter((char) => {
const displayName = getDisplayName(char);
const displayEpithets = getDisplayEpithets(char);
const nameMatches = normalizeSearchText(displayName).includes(searchTerm);
const epithetsMatches = displayEpithets.some((epithet) =>
normalizeSearchText(epithet).includes(searchTerm)
);
if (Array.isArray(parsedEpithets)) {
epithetsMatches = parsedEpithets.some((epithet: string) =>
normalizeSearchText(epithet).includes(searchTerm)
);
} else if (typeof parsedEpithets === 'string') {
epithetsMatches = normalizeSearchText(parsedEpithets).includes(searchTerm);
}
} catch {
epithetsMatches = normalizeSearchText(String(char.epithets)).includes(searchTerm);
}
}
return (nameMatches || epithetsMatches) &&
!selectedCharacters.some(selected => selected.id === char.id);
return (nameMatches || epithetsMatches) &&
!selectedCharacters.some((selected) => selected.id === char.id);
});
});
// Reset highlighted index when filtered list changes
$: if (filteredCharacters) {
highlightedIndex = 0;
}
// Scroll highlighted item into view
$: if (dropdownContainer && highlightedIndex >= 0) {
const highlightedButton = dropdownContainer.querySelector(
`button:nth-child(${highlightedIndex + 1})`
) as HTMLElement;
if (highlightedButton) {
highlightedButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
// Reset highlighted index when filtered list changes.
$effect(() => {
const nextFilteredCharacters = filteredCharacters;
if (!nextFilteredCharacters) {
return;
}
}
state.highlightedIndex = 0;
});
function selectCharacter(character: any) {
dispatch('select', character);
searchInput = '';
highlightedIndex = 0;
// Scroll highlighted item into view.
$effect(() => {
const nextFilteredCharacters = filteredCharacters;
if (!state.dropdownContainer || state.highlightedIndex < 0) {
return;
}
if (state.highlightedIndex >= nextFilteredCharacters.length) {
return;
}
const highlightedButton = state.dropdownContainer.querySelector(
`button:nth-child(${state.highlightedIndex + 1})`
) as HTMLElement | null;
highlightedButton?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
});
function selectCharacter(character: CharacterWithRelations) {
onSelect(character);
state.searchInput = '';
state.highlightedIndex = 0;
}
function handleKeydown(event: KeyboardEvent) {
@@ -82,16 +132,19 @@
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
state.highlightedIndex = Math.min(
state.highlightedIndex + 1,
filteredCharacters.length - 1
);
break;
case 'ArrowUp':
event.preventDefault();
highlightedIndex = Math.max(highlightedIndex - 1, 0);
state.highlightedIndex = Math.max(state.highlightedIndex - 1, 0);
break;
case 'Enter':
event.preventDefault();
if (filteredCharacters[highlightedIndex]) {
selectCharacter(filteredCharacters[highlightedIndex]);
if (filteredCharacters[state.highlightedIndex]) {
selectCharacter(filteredCharacters[state.highlightedIndex]);
}
break;
}
@@ -99,44 +152,43 @@
function submitGuess() {
if (filteredCharacters.length === 0) return;
const characterToSelect =
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
const characterToSelect = filteredCharacters[state.highlightedIndex] ?? filteredCharacters[0];
if (characterToSelect) {
selectCharacter(characterToSelect);
}
}
function handleClickOutside(event: MouseEvent) {
if (searchContainer && !searchContainer.contains(event.target as Node)) {
searchInput = '';
if (state.searchContainer && !state.searchContainer.contains(event.target as Node)) {
state.searchInput = '';
}
}
</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">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.components.searchInput.title}</h2>
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
<div bind:this={searchContainer} class="relative w-full">
<div bind:this={state.searchContainer} class="relative w-full">
<input
bind:value={searchInput}
bind:value={state.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"
placeholder="Nom du personnage"
placeholder={$t.game.components.searchInput.placeholder}
type="text"
onkeydown={handleKeydown}
/>
{#if searchInput.length > 0 && filteredCharacters.length > 0}
<div bind:this={dropdownContainer} class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-2xl border border-amber-200/30 bg-slate-900/90 backdrop-blur z-10">
{#if state.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">
{#each filteredCharacters as character, index (character.id)}
<button
class="w-full px-5 py-4 text-left text-base text-slate-100 transition border-b border-slate-800/50 last:border-b-0 flex items-center gap-4 {index === highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}"
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'}"
type="button"
onmouseenter={() => highlightedIndex = index}
onmouseenter={() => (state.highlightedIndex = index)}
onclick={() => selectCharacter(character)}
>
{#if character.pictureUrl}
<img
src={character.pictureUrl}
alt={character.name}
alt={getDisplayName(character)}
loading="lazy"
class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
/>
@@ -146,16 +198,11 @@
</div>
{/if}
<div class="flex-1">
<span class="font-semibold text-amber-100">{character.name}</span>
{#if character.epithets}
{@const parsedEpithets = typeof character.epithets === 'string'
? JSON.parse(character.epithets)
: character.epithets}
{#if Array.isArray(parsedEpithets) && parsedEpithets.length > 0}
<span class="font-semibold text-amber-100">{getDisplayName(character)}</span>
{#if getDisplayEpithets(character).length > 0}
<span class="ml-2 text-xs text-slate-400">
{parsedEpithets.join(', ')}
{getDisplayEpithets(character).join(', ')}
</span>
{/if}
{/if}
</div>
</button>
@@ -166,10 +213,10 @@
<button
type="button"
onclick={submitGuess}
disabled={searchInput.length === 0 || filteredCharacters.length === 0}
disabled={state.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"
>
Valider
{$t.game.components.searchInput.submit}
</button>
</div>
</div>

View File

@@ -0,0 +1,73 @@
<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,257 +1,493 @@
<script lang="ts">
import { formatBounty } from '$lib';
import type { CharacterWithRelations } from '$lib/server/daily-character';
import { language, t } from '$lib/i18n';
export let selectedCharacters: any[];
export let dailyCharacter: any;
export let columnVisibility: any;
export let selectedCharacters: CharacterWithRelations[];
export let dailyCharacter: CharacterWithRelations;
export let columnVisibility: {
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>
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<section
class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"
>
<div class="flex flex-col gap-4">
<div class="flex flex-col items-center gap-4 text-center">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
<p class="text-xs font-semibold tracking-[0.28em] text-amber-100 uppercase">{$t.game.components.guessHistory.title}</p>
</div>
{#if selectedCharacters.length === 0}
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
<p class="text-center text-sm text-slate-200">{$t.game.components.guessHistory.empty}</p>
{:else}
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
<div class="w-max min-w-max mx-auto">
<div class="-mx-6 overflow-x-auto px-6 pb-2 sm:mx-0 sm:px-0">
<div class="mx-auto w-max min-w-max">
<!-- Header -->
<div class="flex gap-1 sm:gap-2 mb-2">
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
<div class="mb-2 flex gap-1 sm:gap-2">
<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.character}
</p>
</div>
{#if columnVisibility.status !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
</div>
<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.status}
</p>
</div>
{/if}
{#if columnVisibility.gender !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
</div>
<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.gender}
</p>
</div>
{/if}
{#if columnVisibility.affiliations !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
</div>
{#if columnVisibility.affiliation !== 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.affiliations}
</p>
</div>
{/if}
{#if columnVisibility.devilFruitType !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
</div>
<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.fruit}
</p>
</div>
{/if}
{#if columnVisibility.haki !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
</div>
<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.haki}
</p>
</div>
{/if}
{#if columnVisibility.bounty !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
</div>
<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.bounty}
</p>
</div>
{/if}
{#if columnVisibility.height !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
</div>
<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.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 columnVisibility.origin !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
</div>
<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.origin}
</p>
</div>
{/if}
{#if columnVisibility.arc !== false}
<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">
<p class="text-[9px] sm:text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
</div>
<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.arc}
</p>
</div>
{/if}
</div>
<!-- Rows -->
{#each selectedCharacters as character (character.id)}
<div class="flex gap-1 sm:gap-2 mb-2">
<div class="mb-2 flex gap-1 sm:gap-2">
<!-- Personnage -->
<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">
<div
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}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
target="_blank"
rel="noopener noreferrer"
class="block w-full h-full"
>
<img
src={character.pictureUrl}
alt={character.name}
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
/>
</a>
<a
href={getWikiBaseUrl() + getWikiUrl(character)}
target="_blank"
rel="noopener noreferrer"
class="block h-full w-full"
>
<img
src={character.pictureUrl}
alt={getDisplayName(character)}
class="h-full w-full cursor-pointer object-cover transition-opacity hover:opacity-80"
/>
</a>
{:else}
<div class="w-full h-full bg-slate-800 flex items-center justify-center 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>
<div
class="flex h-full w-full items-center justify-center bg-slate-800 p-1 sm:p-2"
>
<span
class="line-clamp-3 text-center text-xs font-semibold sm:text-sm md:text-xl"
>{getDisplayName(character)}</span
>
</div>
{/if}
</div>
<!-- Vivant / Mort -->
{#if columnVisibility.status !== false}
<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">
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">
{character.status === 'Alive'
? 'Vivant'
: character.status === 'Deceased' || character.status === 'Dead'
? 'Mort'
: character.status === 'Unknown'
? 'Inconnu'
: character.status === null
? '-'
: character.status || 'Inconnu'}
</p>
</div>
<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.status ===
dailyCharacter.status
? 'bg-emerald-600/90'
: '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">
{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}
<!-- Genre -->
{#if columnVisibility.gender !== false}
<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">
<p class="text-xs sm:text-sm md:text-base font-bold text-white text-center">
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
</p>
</div>
<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.gender ===
dailyCharacter.gender
? 'bg-emerald-600/90'
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2"
>
<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}
<!-- Affiliations -->
{#if columnVisibility.affiliations !== false}
<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 {(() => {
try {
const charAff = typeof character.affiliations === 'string'
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
: character.affiliations;
const dailyAff = typeof dailyCharacter.affiliations === 'string'
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
: dailyCharacter.affiliations;
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
const charHasAff = charFirstAff && charFirstAff.trim() !== '';
const dailyHasAff = dailyFirstAff && dailyFirstAff.trim() !== '';
// If both have the same affiliation status and value
if (charHasAff === dailyHasAff && ((!charHasAff && !dailyHasAff) || charFirstAff === dailyFirstAff)) {
return 'bg-emerald-600/90';
}
return '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 columnVisibility.affiliation !== 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 {getDislayAffiliation(character) === getDislayAffiliation(dailyCharacter)
? 'bg-emerald-600/90'
: '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">
{getDislayAffiliation(character) || $t.game.components.guessHistory.unknown}
</p>
</div>
{/if}
<!-- Fruit -->
{#if columnVisibility.devilFruitType !== false}
<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">
{#if character.devilFruitType}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">{character.devilFruitType}</p>
{:else}
<p class="text-2xl sm:text-3xl md:text-5xl font-bold text-white text-center"></p>
{/if}
</div>
<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.devilFruitType ===
dailyCharacter.devilFruitType
? 'bg-emerald-600/90'
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2"
>
{#if character.devilFruitType}
<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}
<!-- Haki -->
{#if columnVisibility.haki !== false}
<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 {(() => {
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
return 'bg-emerald-600/90';
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
return 'bg-yellow-600/80';
} else {
return 'bg-red-900/60';
}
})()} p-1 sm:p-2 flex items-center justify-center">
<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}
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
<span class="text-2xl sm:text-3xl md:text-5xl"></span>
{/if}
</p>
</div>
<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 {(() => {
if (
character.hakiObservation === dailyCharacter.hakiObservation &&
character.hakiArmament === dailyCharacter.hakiArmament &&
character.hakiConqueror === dailyCharacter.hakiConqueror
) {
return 'bg-emerald-600/90';
} else if (
(character.hakiObservation && dailyCharacter.hakiObservation) ||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
(character.hakiConqueror && dailyCharacter.hakiConqueror)
) {
return 'bg-yellow-600/80';
} else {
return 'bg-red-900/60';
}
})()} flex items-center justify-center p-1 sm:p-2"
>
<p class="text-center text-sm font-bold text-white sm:text-lg md:text-2xl">
{#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}
<!-- Prime -->
{#if columnVisibility.bounty !== false}
<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">
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
<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.bounty ===
dailyCharacter.bounty
? '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);
clip-path: {character.bounty > dailyCharacter.bounty
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"></div>
{/if}
{#if character.bounty != null}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
{:else}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"
></div>
{/if}
{#if character.bounty != null}
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{formatBounty(character.bounty)} ฿
</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}
<!-- Taille -->
{#if columnVisibility.height !== false}
<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">
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
<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.height ===
dailyCharacter.height
? '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);
clip-path: {character.height > dailyCharacter.height
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"></div>
{/if}
{#if character.height}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"
></div>
{/if}
{#if character.height}
<p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
>
{character.height} m
</p>
{:else}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
<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}
<!-- 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}
<!-- Origine -->
{#if columnVisibility.origin !== false}
<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">
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
</div>
<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 {hasMatchingOrigin(character, dailyCharacter)
? 'bg-emerald-600/90'
: '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}
<!-- Arc -->
{#if columnVisibility.arc !== false}
<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">
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
<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 {hasMatchingArc(character, dailyCharacter)
? 'bg-emerald-600/90'
: '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);
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"></div>
{/if}
<p class="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
</div>
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
"
></div>
{/if}
<p
class="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}
</div>
{/each}

View File

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

View File

@@ -0,0 +1,104 @@
<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,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { User } from 'better-auth/types';
import { resolve } from '$app/paths';
interface Props {
user: (User & { isAdmin?: boolean }) | null;
@@ -59,7 +60,7 @@
{user.name?.charAt(0).toUpperCase() || 'U'}
</div>
{/if}
<span class="max-w-[150px] truncate text-sm font-semibold text-slate-100">
<span class="max-w-37.5 truncate text-sm font-semibold text-slate-100">
{user.name || 'Utilisateur'}
</span>
<svg
@@ -77,15 +78,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"
>
<a
href="/profile"
href={resolve("/profile")}
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"
>
Voir mon profil
</a>
{#if (user as any).isAdmin}
{#if (user).isAdmin}
<a
href="/admin"
href={resolve("/admin")}
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"
>
@@ -102,7 +103,7 @@
{/if}
{:else}
<a
href="/login"
href={resolve("/login")}
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

View File

@@ -1,26 +1,42 @@
<script lang="ts">
export let selectedCharacter: any;
export let selectedCharacters: any[];
import type { CharacterWithRelations } from "$lib/server/daily-character";
import { language, t } from '$lib/i18n';
export let selectedCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[];
export let isGeckoMoriaWin: boolean = false;
const oneTryMessages = ['Tricheur 👀', '1 essai ? Avoue, tu avais la réponse 😏', 'Premier coup direct... suspect 🤨'];
const twoTryMessages = ['Bien joué ! ⚡', 'Deux essais, propre ! 👏', 'Tu chauffes vite, bien joué 🔥'];
const tenPlusMessages = [
'${attempts} essais... même un escargophone aurait trouvé plus vite 📞',
'${attempts} tentatives ? Le Grand Line est moins long que ça 😵',
'${attempts} essais : performance légendaire... dans le mauvais sens 🫠'
];
const fivePlusMessages = [
"${attempts} essais ? On va dire que c'était pour le suspense 😅",
'Ça en fait des essais... mais au moins tu y es arrivé 😬',
'Tu ne lâches rien, même après plusieurs essais 😂'
];
const defaultMessages = ['Pas mal du tout !', 'Bien tenté, bon rythme 👍', 'Ça se passe bien, continue comme ça ✨'];
$: isFrench = $language === 'fr';
const pickMessage = (messages: string[]) => messages[Math.floor(Math.random() * messages.length)];
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);
}
@@ -39,31 +55,34 @@
$: attempts = selectedCharacters.length;
$: attemptMessage = getAttemptMessage(attempts);
$: attemptWord = selectedCharacters.length > 1
? $t.game.components.winPanel.attemptPlural
: $t.game.components.winPanel.attemptSingular;
</script>
{#if isGeckoMoriaWin}
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
<div class="text-center">
<div class="text-3xl mb-2">🌑</div>
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<h2 class="text-xl font-bold text-slate-300 mb-1">{$t.game.components.winPanel.moriaTitle}</h2>
<p class="text-sm text-slate-400">{$t.game.components.winPanel.moriaPrefix} {selectedCharacters.length} {attemptWord} !</p>
<p class="text-xs text-slate-300 mt-1">{attemptMessage}</p>
<div class="mt-3">
{#if selectedCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + selectedCharacter.url}
href={getWikiBaseUrl() + getWikiUrl(selectedCharacter)}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={selectedCharacter.pictureUrl}
alt={selectedCharacter.name}
alt={getDisplayName(selectedCharacter)}
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
/>
</a>
{/if}
<p class="mt-2 text-lg font-bold text-slate-200">{selectedCharacter.name}</p>
<p class="mt-2 text-lg font-bold text-slate-200">{getDisplayName(selectedCharacter)}</p>
</div>
</div>
</div>
@@ -71,25 +90,25 @@
<div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur">
<div class="text-center">
<div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<h2 class="text-xl font-bold text-emerald-400 mb-1">{$t.game.components.winPanel.winTitle}</h2>
<p class="text-sm text-emerald-300">{$t.game.components.winPanel.winPrefix} {selectedCharacters.length} {attemptWord} !</p>
<p class="text-xs text-emerald-200 mt-1">{attemptMessage}</p>
<div class="mt-3">
{#if selectedCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + selectedCharacter.url}
href={getWikiBaseUrl() + getWikiUrl(selectedCharacter)}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={selectedCharacter.pictureUrl}
alt={selectedCharacter.name}
alt={getDisplayName(selectedCharacter)}
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">{selectedCharacter.name}</p>
<p class="mt-2 text-lg font-bold text-white">{getDisplayName(selectedCharacter)}</p>
</div>
</div>
</div>

View File

@@ -1,5 +1,57 @@
<script lang="ts">
export let yesterdayCharacter: any;
import type { CharacterWithRelations } from "$lib/server/daily-character";
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>
<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">
@@ -8,43 +60,52 @@
{#if yesterdayCharacter.pictureUrl}
<img
src={yesterdayCharacter.pictureUrl}
alt={yesterdayCharacter.name}
alt={getDisplayName(yesterdayCharacter)}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/>
{:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
{$t.game.components.yesterdayCharacter.photo}
</div>
{/if}
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets}
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.components.yesterdayCharacter.title}</p>
<p class="mt-2 text-lg font-semibold text-white">{getDisplayName(yesterdayCharacter)}</p>
{#if getDisplayEpithets(yesterdayCharacter).length > 0}
<p class="mt-1 text-sm text-slate-400">
{typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
{getDisplayEpithets(yesterdayCharacter).join(', ')}
</p>
{/if}
</div>
<a
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
>
Voir la page
</a>
{#if isFrench}
<a
href="https://onepiece.fandom.com/fr/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>
{: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>
{:else}
<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">
Photo
{$t.game.components.yesterdayCharacter.photo}
</div>
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.components.yesterdayCharacter.title}</p>
<p class="mt-2 text-lg font-semibold text-white">{$t.game.components.yesterdayCharacter.none}</p>
<p class="mt-1 text-sm text-slate-200">{$t.game.components.yesterdayCharacter.noneAvailable}</p>
</div>
</div>
{/if}

233
src/lib/i18n/en.json Normal file
View File

@@ -0,0 +1,233 @@
{
"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"
}
}
}
}

233
src/lib/i18n/fr.json Normal file
View File

@@ -0,0 +1,233 @@
{
"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"
}
}
}
}

51
src/lib/i18n/index.ts Normal file
View File

@@ -0,0 +1,51 @@
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,6 +9,15 @@ export const auth = betterAuth({
baseURL: env.ORIGIN,
secret: env.BETTER_AUTH_SECRET || 'secret',
database: drizzleAdapter(db, { provider: 'sqlite' }),
user: {
additionalFields: {
username: {
type: 'string',
required: true,
unique: true
}
}
},
emailAndPassword: { enabled: true },
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 { arc, character, characterHistory, characterOverride, devilFruit } from '$lib/server/db/schema';
import { desc, eq, inArray, and } from 'drizzle-orm';
import { arc, character, characterHistory, devilFruit, type Character } from '$lib/server/db/schema';
import { desc, eq, and } from 'drizzle-orm';
// Generate or get random seed for daily character selection
const RANDOM_SEED = Math.random();
@@ -8,9 +8,11 @@ const RANDOM_SEED = Math.random();
const characterWithRelationsSelect = {
id: character.id,
name: character.name,
frName: character.frName,
gender: character.gender,
age: character.age,
affiliations: character.affiliations,
affiliation: character.affiliation,
frAffiliation: character.frAffiliation,
devilFruitId: character.devilFruitId,
devilFruitName: devilFruit.name,
devilFruitType: devilFruit.type,
@@ -20,23 +22,26 @@ const characterWithRelationsSelect = {
bounty: character.bounty,
height: character.height,
origin: character.origin,
frOrigin: character.frOrigin,
firstAppearance: character.firstAppearance,
pictureUrl: character.pictureUrl,
epithets: character.epithets,
frEpithets: character.frEpithets,
status: character.status,
url: character.url,
frUrl: character.frUrl,
arcId: character.arcId,
arcName: arc.name
arcName: arc.name,
frArcName: arc.frName,
};
export type CharacterWithRelations = typeof character.$inferSelect & {
export type CharacterWithRelations = Character & {
devilFruitName: string | null;
devilFruitType: string | null;
arcName: string | null;
frArcName: string | null;
};
type CharacterOverrideRow = typeof characterOverride.$inferSelect;
type RelationMaps = {
arcNameById: Map<string, string | null>;
devilFruitById: Map<string, { name: string | null; type: string | null }>;
@@ -46,102 +51,6 @@ function isNotNullish<T>(value: T | null | undefined): value is T {
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 {
return normalizeDay(date).getTime();
}
@@ -161,26 +70,22 @@ function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): C
}
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
const characters = (await db
return (await db
.select(characterWithRelationsSelect)
.from(character)
.leftJoin(arc, eq(character.arcId, arc.id))
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
.where(eq(character.isInDailyMode, true))
.all()) as CharacterWithRelations[];
return applyCharacterOverrides(characters);
}
export async function getAllCharacters(): Promise<CharacterWithRelations[]> {
const characters = (await db
return (await db
.select(characterWithRelationsSelect)
.from(character)
.leftJoin(arc, eq(character.arcId, arc.id))
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
.all()) as CharacterWithRelations[];
return applyCharacterOverrides(characters);
}
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
@@ -196,8 +101,7 @@ export async function getCharacterById(characterId: string): Promise<CharacterWi
return null;
}
const [overriddenCharacter] = await applyCharacterOverrides([found as CharacterWithRelations]);
return overriddenCharacter ?? null;
return found as CharacterWithRelations
}
export async function getOrCreateTodayCharacter(

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import ProfileButton from '$lib/components/ProfileButton.svelte';
import { resolve } from '$app/paths';
let { children, data } = $props();
@@ -29,9 +30,9 @@
<h2 class="text-lg font-black uppercase tracking-[0.15em] text-amber-50">Admin</h2>
</div>
<nav class="flex-1 space-y-2 px-3">
{#each navItems as item}
{#each navItems as item (item.label)}
<a
href={item.href}
href={resolve(item.href)}
class={`flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium transition-colors ${
isActive(item.href, $page.url.pathname)
? 'bg-amber-600 text-white'
@@ -45,7 +46,7 @@
</nav>
<div class="border-t border-white/5 p-3">
<a
href="/"
href={resolve('/')}
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"
>

View File

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

View File

@@ -11,7 +11,7 @@ const EXEC_OPTIONS = {
maxBuffer: 50 * 1024 * 1024
};
async function upsertCharacterFromScrapeValidation(characterId: string): Promise<boolean> {
async function applyCharacterChangeFromScrapeValidation(characterId: string): Promise<boolean> {
const [scraped] = await db
.select()
.from(characterScrapeValidation)
@@ -21,14 +21,21 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
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,
affiliations: scraped.affiliations,
affiliation: scraped.affiliation,
frAffiliation: scraped.frAffiliation,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
@@ -36,20 +43,25 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
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
url: scraped.url,
frUrl: scraped.frUrl,
})
.onConflictDoUpdate({
target: character.id,
set: {
name: scraped.name,
frName: scraped.frName,
gender: scraped.gender,
age: scraped.age,
affiliations: scraped.affiliations,
affiliation: scraped.affiliation,
frAffiliation: scraped.frAffiliation,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
@@ -57,12 +69,15 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
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
url: scraped.url,
frUrl: scraped.frUrl
}
});
@@ -79,7 +94,7 @@ export async function load() {
// Compare and categorize changes
const changes: {
type: 'new' | 'modified';
type: 'new' | 'modified' | 'deleted';
id: string;
scraped: (typeof scrapedCharacters)[0];
current?: (typeof currentCharacters)[0];
@@ -89,6 +104,18 @@ export async function load() {
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({
@@ -101,9 +128,11 @@ export async function load() {
const differences: Record<string, { current: any; scraped: any }> = {};
const fieldsToCompare = [
'name',
'frName',
'gender',
'age',
'affiliations',
'affiliation',
'frAffiliation',
'devilFruitId',
'hakiObservation',
'hakiArmament',
@@ -111,12 +140,15 @@ export async function load() {
'bounty',
'height',
'origin',
'frOrigin',
'firstAppearance',
'pictureUrl',
'epithets',
'frEpithets',
'status',
'arcId',
'url'
'url',
'frUrl'
];
for (const field of fieldsToCompare) {
@@ -144,11 +176,16 @@ export async function load() {
}
}
const typeOrder: Record<'new' | 'modified' | 'deleted', number> = {
new: 0,
modified: 1,
deleted: 2
};
return {
changes: changes.sort((a, b) => {
// Show 'new' first, then 'modified'
if (a.type !== b.type) {
return a.type === 'new' ? -1 : 1;
return typeOrder[a.type] - typeOrder[b.type];
}
return a.id.localeCompare(b.id);
})
@@ -209,10 +246,10 @@ export const actions = {
return { success: false, message: 'characterId is required' };
}
const applied = await upsertCharacterFromScrapeValidation(characterId);
const applied = await applyCharacterChangeFromScrapeValidation(characterId);
return {
success: applied,
message: applied ? 'Character applied successfully' : 'Character not found in scrape validation table'
message: applied ? 'Character change applied successfully' : 'Character not found in scrape validation table'
};
},
@@ -221,7 +258,7 @@ export const actions = {
let appliedCount = 0;
for (const scraped of scrapedCharacters) {
const applied = await upsertCharacterFromScrapeValidation(scraped.id);
const applied = await applyCharacterChangeFromScrapeValidation(scraped.id);
if (applied) {
appliedCount++;
}

View File

@@ -1,17 +1,30 @@
<script lang="ts">
import { page } from '$app/stores';
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.filter((c: any) => c.type === 'new'));
const modifiedCharacters = $derived(data.changes.filter((c: any) => c.type === 'modified'));
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 fandomUrl(path: string | null | undefined): string {
if (!path) return 'https://onepiece.fandom.com/fr/wiki';
return `https://onepiece.fandom.com/fr/wiki/${path}`;
}
function formatValue(value: any): string {
function formatValue(value: unknown): string {
if (value === null || value === undefined) {
return '—';
}
@@ -23,13 +36,6 @@
}
return String(value);
}
function getDifferenceColor(current: any, scraped: any): string {
if (JSON.stringify(current) === JSON.stringify(scraped)) {
return 'text-gray-400';
}
return 'text-amber-300';
}
</script>
<svelte:head>
@@ -39,7 +45,7 @@
<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</p>
<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"
@@ -56,7 +62,7 @@
{#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 > 0}
{#if newCharacters.length + modifiedCharacters.length + deletedCharacters.length > 0}
<form method="POST" action="?/acceptAll" class="mt-4">
<button
type="submit"
@@ -80,9 +86,9 @@
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
{#if change.scraped.pictureUrl}
<a href={fandomUrl(change.scraped.url)} target="_blank" rel="noopener noreferrer">
<a href="https://onepiece.fandom.com/fr/wiki/{change.scraped.url}" target="_blank" rel="noopener noreferrer">
<img
src={change.scraped.pictureUrl}
src={change.scraped.pictureUrl ?? undefined}
alt={change.scraped.name}
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
/>
@@ -139,10 +145,10 @@
<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={fandomUrl(change.current?.url ?? change.scraped.url)} target="_blank" rel="noopener noreferrer">
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
<img
src={change.current.pictureUrl}
alt={change.current.name}
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>
@@ -165,7 +171,7 @@
{#if change.differences}
<div class="space-y-3">
{#each Object.entries(change.differences) as [field, diff]}
{#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">
@@ -188,7 +194,49 @@
</section>
{/if}
{#if newCharacters.length === 0 && modifiedCharacters.length === 0}
<!-- 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>

View File

@@ -1,22 +1,32 @@
import { db } from '$lib/server/db';
import { character, devilFruit, arc, characterOverride } from '$lib/server/db/schema';
import { character, devilFruit, arc, type Status } from '$lib/server/db/schema';
import { eq, sql } from 'drizzle-orm';
import { fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { env } from '$env/dynamic/private';
// 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 () => {
const [charactersData, devilFruits, arcs, overrides, statusesData, gendersData] = await Promise.all([
let [characters, devilFruits, arcs, statusesData, gendersData] = await Promise.all([
db
.select({
id: character.id,
name: character.name,
gender: character.gender,
age: character.age,
affiliations: character.affiliations,
affiliation: character.affiliation,
devilFruitId: character.devilFruitId,
hakiObservation: character.hakiObservation,
hakiArmament: character.hakiArmament,
@@ -26,7 +36,7 @@ export const load: PageServerLoad = async () => {
origin: character.origin,
firstAppearance: character.firstAppearance,
pictureUrl: character.pictureUrl,
epithets: character.epithets,
epithets: normalizeArray(character.epithets),
status: character.status,
url: character.url,
arcId: character.arcId,
@@ -41,7 +51,6 @@ export const load: PageServerLoad = async () => {
.orderBy(character.name),
db.select().from(devilFruit).orderBy(devilFruit.name),
db.select().from(arc).orderBy(arc.name),
db.select().from(characterOverride),
db.selectDistinct({ status: character.status })
.from(character)
.where(sql`${character.status} IS NOT NULL AND ${character.status} != ''`),
@@ -50,76 +59,13 @@ export const load: PageServerLoad = async () => {
.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]));
// Create maps for arcs and devil fruits to lookup names by ID
const arcMap = new Map(arcs.map((a) => [a.id, a.name]));
const devilFruitMap = new Map(devilFruits.map((f) => [f.id, { name: f.name, type: f.type }]));
// 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;
};
// 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];
}
});
// Update arcName if arcId was overridden
if (override.arcId !== null && override.arcId !== undefined) {
displayValues.arcName = arcMap.get(override.arcId) || null;
}
// Update devilFruitName and devilFruitType if devilFruitId was overridden
if (override.devilFruitId !== null && override.devilFruitId !== undefined) {
const fruit = devilFruitMap.get(override.devilFruitId);
displayValues.devilFruitName = fruit?.name || null;
displayValues.devilFruitType = fruit?.type || null;
}
}
// Pre-normalize arrays (epithets, affiliations) for performance
displayValues.epithets = normalizeArray(displayValues.epithets);
displayValues.affiliations = normalizeArray(displayValues.affiliations);
// Create search text for epithets
displayValues.epithetsSearchText = Array.isArray(displayValues.epithets)
? displayValues.epithets.join(' ').toLowerCase()
: (displayValues.epithets || '').toLowerCase();
return {
...char,
override,
displayValues
};
});
return {
characters: charactersWithOverrides,
characters,
devilFruits,
arcs,
availableStatuses: statusesData
.map(s => s.status)
.filter((s): s is string => !!s)
.filter((s): s is Status => !!s)
.sort((a, b) => a.localeCompare(b)),
availableGenders: gendersData
.map(g => g.gender)
@@ -129,112 +75,6 @@ export const load: PageServerLoad = async () => {
};
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> = {};
// Handle file upload
const pictureFile = formData.get('pictureFile') as File;
const hasUploadedPicture = !!pictureFile && pictureFile.size > 0;
if (hasUploadedPicture) {
try {
const uploadsDir = env.UPLOADS_DIR || join(process.cwd(),'uploads');
if (!existsSync(uploadsDir)) {
mkdirSync(uploadsDir, { recursive: true });
}
// Get file extension
const extension = pictureFile.name.split('.').pop();
const filename = `${id}.${extension}`;
const filepath = join(uploadsDir, filename);
// Convert file to buffer and save
const buffer = Buffer.from(await pictureFile.arrayBuffer());
await writeFile(filepath, buffer);
// Update pictureUrl to point to the handler route
updates.pictureUrl = `/uploads/${filename}`;
} catch (error) {
console.error('File upload error:', error);
return fail(500, { error: 'Failed to upload file' });
}
}
formData.forEach((value, key) => {
if (key !== 'id' && key !== 'pictureFile') {
if (hasUploadedPicture && key === 'pictureUrl') {
return;
}
// Handle integers (age, bounty, height)
if (key === 'age' || key === 'bounty' || key === 'height') {
const strValue = value as string;
updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null;
}
// Handle text IDs (devilFruitId, arcId)
else if (key === 'devilFruitId' || key === 'arcId') {
const strValue = value as string;
updates[key] = strValue && strValue !== '' ? 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 }) => {
if (!locals.user?.isAdmin) {
return fail(401, { error: 'Unauthorized' });

View File

@@ -15,13 +15,11 @@
let filterGender = $state('all');
let filterArc = $state('all');
let filterHaki = $state<'all' | 'observation' | 'armament' | 'conqueror' | 'none'>('all');
let selectedCharacterId = $state<string | null>(null);
let isEditModalOpen = $state(false);
let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let dailyModeToast = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let selectedChar = $state<any>(null);
let showOriginalValue = $state<Record<string, boolean>>({});
const showDailyModeToast = (type: 'success' | 'error', text: string) => {
dailyModeToast = { type, text };
@@ -38,12 +36,6 @@
}
};
const getFandomUrl = (url: string | null | undefined) => {
if (!url) return null;
if (url.startsWith('http://') || url.startsWith('https://')) return url;
return `https://onepiece.fandom.com/fr/wiki/${url}`;
};
let editForm = $state<any>({
id: '',
name: '',
@@ -52,7 +44,7 @@
bounty: 0,
height: 0,
origin: '',
affiliations: '',
affiliation: '',
epithets: '',
pictureUrl: '',
url: '',
@@ -71,23 +63,22 @@
const matchesSearch =
normalizedQuery === '' ||
char.displayValues.name.toLowerCase().includes(normalizedQuery) ||
char.displayValues.epithetsSearchText.includes(normalizedQuery);
char.name.toLowerCase().includes(normalizedQuery);
const matchesDaily =
filterDaily === 'all' ||
(filterDaily === 'daily' && char.displayValues.isInDailyMode) ||
(filterDaily === 'not-daily' && !char.displayValues.isInDailyMode);
const matchesStatus = filterStatus === 'all' || (char.displayValues.status || '') === filterStatus;
const matchesGender = filterGender === 'all' || (char.displayValues.gender || '') === filterGender;
(filterDaily === 'daily' && char.isInDailyMode) ||
(filterDaily === 'not-daily' && !char.isInDailyMode);
const matchesStatus = filterStatus === 'all' || (char.status || '') === filterStatus;
const matchesGender = filterGender === 'all' || (char.gender || '') === filterGender;
const matchesArc =
filterArc === 'all' ||
String(char.displayValues.arcId ?? '') === filterArc;
String(char.arcId ?? '') === filterArc;
const matchesHaki =
filterHaki === 'all' ||
(filterHaki === 'observation' && !!char.displayValues.hakiObservation) ||
(filterHaki === 'armament' && !!char.displayValues.hakiArmament) ||
(filterHaki === 'conqueror' && !!char.displayValues.hakiConqueror) ||
(filterHaki === 'none' && !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror);
(filterHaki === 'observation' && !!char.hakiObservation) ||
(filterHaki === 'armament' && !!char.hakiArmament) ||
(filterHaki === 'conqueror' && !!char.hakiConqueror) ||
(filterHaki === 'none' && !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror);
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
});
@@ -98,7 +89,6 @@
};
const openEditModal = (char: any) => {
selectedCharacterId = char.id;
selectedChar = char;
const override = char.override || {};
@@ -111,7 +101,7 @@
bounty: override.bounty ?? null,
height: override.height ?? null,
origin: override.origin ?? '',
affiliations: override.affiliations ?? '',
affiliation: override.affiliation ?? '',
epithets: override.epithets ?? '',
pictureUrl: override.pictureUrl ?? '',
url: override.url ?? '',
@@ -123,13 +113,11 @@
arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : (char.arcId || ''),
status: override.status ?? ''
};
showOriginalValue = {};
isEditModalOpen = true;
};
const closeModal = () => {
isEditModalOpen = false;
selectedCharacterId = null;
selectedChar = null;
editForm = {
id: '',
@@ -139,7 +127,7 @@
bounty: 0,
height: 0,
origin: '',
affiliations: '',
affiliation: '',
epithets: '',
pictureUrl: '',
url: '',
@@ -179,6 +167,7 @@
}, 3000);
}
} catch (error) {
console.error('Error deleting character:', error);
saveMessage = {
type: 'error',
text: 'Error deleting character'
@@ -221,7 +210,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"
>
<option value="all">All Statuses</option>
{#each data.availableStatuses as status}
{#each data.availableStatuses as status (status)}
<option value={status}>{status}</option>
{/each}
</select>
@@ -230,7 +219,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"
>
<option value="all">All Genders</option>
{#each data.availableGenders as gender}
{#each data.availableGenders as gender (gender)}
<option value={gender}>{gender}</option>
{/each}
</select>
@@ -239,8 +228,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"
>
<option value="all">All Arcs</option>
{#each data.arcs as arc}
<option value={String(arc.id)}>{arc.name}</option>
{#each data.arcs as arc (arc.id)}
<option value={arc.id}>{arc.name}</option>
{/each}
</select>
<select
@@ -284,119 +273,115 @@
</tr>
</thead>
<tbody>
{#each filteredCharacters as char}
{#each filteredCharacters as char (char.id)}
<tr class="border-b border-white/5 hover:bg-slate-800/50">
<!-- Character -->
<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' : ''}">
<td class="px-4 py-4 text-sm text-white w-64 max-w-64">
<div class="flex items-center gap-3 min-w-0">
{#if getFandomUrl(char.displayValues.url)}
{#if char.url}
<a
href={getFandomUrl(char.displayValues.url)}
href={"https://onepiece.fandom.com/wiki/" + char.url}
target="_blank"
rel="noopener noreferrer"
class="flex-shrink-0 transition-opacity hover:opacity-80"
class="shrink-0 transition-opacity hover:opacity-80"
>
{#if char.displayValues.pictureUrl}
{#if char.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.name}
src={char.pictureUrl}
alt={char.name}
loading="lazy"
class="h-10 w-10 rounded-full object-cover"
/>
{:else}
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.displayValues.name?.charAt(0).toUpperCase() || '?'}
{char.name?.charAt(0).toUpperCase() || '?'}
</div>
{/if}
</a>
{:else}
{#if char.displayValues.pictureUrl}
{#if char.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.name}
src={char.pictureUrl}
alt={char.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}
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.displayValues.name?.charAt(0).toUpperCase() || '?'}
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.name?.charAt(0).toUpperCase() || '?'}
</div>
{/if}
{/if}
<div class="flex flex-col min-w-0">
{#if getFandomUrl(char.displayValues.url)}
{#if char.url}
<a
href={getFandomUrl(char.displayValues.url)}
href="https://onepiece.fandom.com/wiki/{char.url}"
target="_blank"
rel="noopener noreferrer"
class="font-medium truncate text-white hover:text-amber-200 hover:underline"
>
{char.displayValues.name}
{char.name}
</a>
{:else}
<span class="font-medium truncate">{char.displayValues.name}</span>
<span class="font-medium truncate">{char.name}</span>
{/if}
{#if char.displayValues.epithets}
{#if char.epithets}
<span class="text-xs text-gray-500 truncate">
{Array.isArray(char.displayValues.epithets)
? char.displayValues.epithets.join(', ')
: char.displayValues.epithets}
{Array.isArray(char.epithets)
? char.epithets.join(', ')
: char.epithets}
</span>
{/if}
</div>
</div>
</td>
<!-- Status -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'status') ? 'bg-amber-500/10' : ''}">{char.displayValues.status || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.status || '-'}</td>
<!-- Gender -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'gender') ? 'bg-amber-500/10' : ''}">{char.displayValues.gender || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.gender || '-'}</td>
<!-- Affiliations -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'affiliations') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.affiliations}
{#if Array.isArray(char.displayValues.affiliations) && char.displayValues.affiliations.length > 0}
<span class="inline-block" title={char.displayValues.affiliations.join(', ')}>{char.displayValues.affiliations[0]}</span>
{:else}
{char.displayValues.affiliations}
{/if}
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.affiliation}
{char.affiliation}
{:else}
-
{/if}
</td>
<!-- Fruit -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'devilFruitId') ? 'bg-amber-500/10' : ''}">{char.displayValues.devilFruitName || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.devilFruitName || '-'}</td>
<!-- Haki -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'hakiObservation') || isFieldOverridden(char, 'hakiArmament') || isFieldOverridden(char, 'hakiConqueror') ? 'bg-amber-500/10' : ''}">
<td class="px-4 py-4 text-sm">
<div class="flex gap-1">
{#if char.displayValues.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if char.displayValues.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if char.displayValues.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror}
{#if char.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if char.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if char.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror}
<span class="text-gray-400">-</span>
{/if}
</div>
</td>
<!-- Bounty -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'bounty') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.bounty != null}
{formatBounty(char.displayValues.bounty)} ฿
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.bounty != null}
{formatBounty(char.bounty)} ฿
{:else}
-
{/if}
</td>
<!-- Height -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'height') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.height}
{char.displayValues.height} m
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.height}
{char.height} m
{:else}
-
{/if}
</td>
<!-- Origin -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'origin') ? 'bg-amber-500/10' : ''}">{char.displayValues.origin || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.origin || '-'}</td>
<!-- Arc -->
<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>
<td class="px-4 py-4 text-sm text-gray-400">{char.arcName || '-'}</td>
<!-- Daily Mode -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'isInDailyMode') ? 'bg-amber-500/10' : ''}">
<td class="px-4 py-4 text-sm">
<form
method="POST"
action="?/toggleDailyMode"
@@ -414,11 +399,11 @@
}}
>
<input type="hidden" name="id" value={char.id} />
<input type="hidden" name="isInDailyMode" value={(!char.displayValues.isInDailyMode).toString()} />
<input type="hidden" name="isInDailyMode" value={(!char.isInDailyMode).toString()} />
<label class="flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={char.displayValues.isInDailyMode}
checked={char.isInDailyMode}
onchange={(e) => {
const form = e.currentTarget.closest('form');
if (form) form.requestSubmit();
@@ -461,7 +446,7 @@
{/if}
{#if dailyModeToast}
<div class="fixed right-6 top-6 z-[60]">
<div class="fixed right-6 top-6 z-60">
<div
class={`rounded-lg border px-4 py-3 text-sm font-medium shadow-lg backdrop-blur ${
dailyModeToast.type === 'success'
@@ -628,7 +613,7 @@
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
>
<option value="">None</option>
{#each data.arcs as arc}
{#each data.arcs as arc (arc.id)}
<option value={arc.id}>{arc.name}</option>
{/each}
</select>
@@ -651,7 +636,7 @@
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
>
<option value="">None</option>
{#each data.devilFruits as fruit}
{#each data.devilFruits as fruit (fruit.id)}
<option value={fruit.id}>{fruit.name}</option>
{/each}
</select>

View File

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

View File

@@ -11,7 +11,6 @@
let searchQuery = $state('');
let filterType = $state<'all' | 'Paramecia' | 'Zoan' | 'Logia' | 'Unknown'>('all');
let isEditModalOpen = $state(false);
let selectedFruitId = $state<string | null>(null);
let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
@@ -33,14 +32,12 @@
});
const openEditModal = (fruit: any) => {
selectedFruitId = fruit.id;
editForm = { ...fruit };
isEditModalOpen = true;
};
const closeModal = () => {
isEditModalOpen = false;
selectedFruitId = null;
editForm = {
id: '',
name: '',
@@ -88,6 +85,7 @@
}, 3000);
}
} catch (error) {
console.error('Error deleting devil fruit:', error);
saveMessage = {
type: 'error',
text: 'Error deleting devil fruit'
@@ -150,7 +148,7 @@
</tr>
</thead>
<tbody>
{#each filteredFruits as fruit}
{#each filteredFruits as fruit (fruit.id)}
<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">
@@ -233,7 +231,7 @@
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"
>
{#each fruitTypes as type}
{#each fruitTypes as type (type)}
<option value={type}>{type}</option>
{/each}
</select>

View File

@@ -13,7 +13,6 @@
let isEditModalOpen = $state(false);
let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; message: string } | null>(null);
let selectedUserId = $state<string | null>(null);
let editForm = $state<any>({
id: '',
@@ -35,7 +34,6 @@
});
const openEditModal = (usr: any) => {
selectedUserId = usr.id;
editForm = { ...usr };
isEditModalOpen = true;
saveMessage = null;
@@ -43,7 +41,6 @@
const closeModal = () => {
isEditModalOpen = false;
selectedUserId = null;
editForm = {
id: '',
name: '',
@@ -120,7 +117,7 @@
</tr>
</thead>
<tbody>
{#each filteredUsers as usr}
{#each filteredUsers as usr (usr.id)}
<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-gray-400">{usr.email}</td>

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import ProfileButton from '$lib/components/ProfileButton.svelte';
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
import { resolve } from '$app/paths';
let { children, data } = $props();
</script>
@@ -7,10 +9,13 @@
<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">
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<a href="/" class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100">
<a href={resolve("/")} class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100">
OnePieceDle
</a>
<ProfileButton user={data.user} />
<div class="flex items-center gap-3">
<LanguageSwitcher />
<ProfileButton user={data.user} />
</div>
</div>
</header>
<main class="pt-20">

View File

@@ -1,7 +1,59 @@
<script lang="ts">
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;
$: 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>
<svelte:head>
@@ -11,7 +63,7 @@
<main
class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"
>
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
<div class="absolute inset-0 bg-linear-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="relative mx-auto flex w-full max-w-6xl flex-col items-center justify-center px-6 py-10">
@@ -21,30 +73,30 @@
OnePieceDle
</h1>
<p class="mt-4 max-w-2xl text-base text-slate-200 sm:text-lg">
Devine le personnage de l'equipage, des marines ou du vaste monde. Chaque indice te rapproche du tresor.
{$t.game.home.heroDescription}
</p>
</div>
<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">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Personnage du jour</h2>
<p class="mt-3 text-lg font-semibold text-white">Nouveau mystere toutes les 24 heures</p>
<p class="mt-2 text-sm text-slate-200">Compare tes essais, debloque des indices et garde ta serie.</p>
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.home.dailyTitle}</h2>
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.dailySubtitle}</p>
<p class="mt-2 text-sm text-slate-200">{$t.game.home.dailyDescription}</p>
<a
href="/daily"
href={resolve("/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"
>
Commencer
{$t.game.home.dailyCta}
</a>
</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">
<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">Des defis sans fin</p>
<p class="mt-2 text-sm text-slate-200">Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.</p>
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.home.infiniteTitle}</h2>
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.infiniteSubtitle}</p>
<p class="mt-2 text-sm text-slate-200">{$t.game.home.infiniteDescription}</p>
<a
href="/infinite"
href={resolve("/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"
>
Jouer
{$t.game.home.infiniteCta}
</a>
</div>
</div>
@@ -54,43 +106,52 @@
{#if yesterdayCharacter.pictureUrl}
<img
src={yesterdayCharacter.pictureUrl}
alt={yesterdayCharacter.name}
alt={getDisplayName(yesterdayCharacter)}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/>
{:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
{$t.game.home.photoFallback}
</div>
{/if}
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets}
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</p>
<p class="mt-2 text-lg font-semibold text-white">{getDisplayName(yesterdayCharacter)}</p>
{#if getDisplayEpithets(yesterdayCharacter).length > 0}
<p class="mt-1 text-sm text-slate-400">
{typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
{getDisplayEpithets(yesterdayCharacter).join(', ')}
</p>
{/if}
</div>
<a
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
>
Voir la page
</a>
{#if isFrench}
<a
href="https://onepiece.fandom.com/fr/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>
{: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>
{:else}
<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">
Photo
{$t.game.home.photoFallback}
</div>
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</p>
<p class="mt-2 text-lg font-semibold text-white">{$t.game.home.noCharacter}</p>
<p class="mt-1 text-sm text-slate-200">{$t.game.home.noYesterdayCharacter}</p>
</div>
</div>
{/if}

View File

@@ -1,10 +1,10 @@
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { config } from '$lib/server/db/schema';
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount } from '$lib/server/daily-character';
import { like } from 'drizzle-orm';
import { character, characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character';
import { and, eq, inArray, like, or } from 'drizzle-orm';
export async function load() {
export async function load(event) {
const characters = await getDailyModeCharacters();
const dailyCharacter = await getOrCreateTodayCharacter(characters);
@@ -17,6 +17,94 @@ export async function load() {
// 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
const columnConfig = await db
.select()
@@ -37,6 +125,7 @@ export async function load() {
dailyCharacter,
yesterdayCharacter,
columnVisibility,
winCount
winCount,
friendsTodayResults
};
}

View File

@@ -1,23 +1,98 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
import GuessHistoryTable from '$lib/components/GuessHistoryTable.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;
let selectedCharacters: any[] = [];
let selectedCharacters: CharacterWithRelations[] = [];
let isLoaded = false;
let isGeckoMoriaWin = false;
let wasOriginAvailable = false;
let wasFruitAvailable = false;
let wasAffiliationAvailable = false;
let showOriginUnlock = false;
let showFruitUnlock = 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
onMount(() => {
@@ -37,8 +112,8 @@
// Reconstruct character objects from IDs
if (Array.isArray(storedIds)) {
selectedCharacters = storedIds
.map((id: string) => data.characters.find((c: any) => c.id === id))
.filter((c: any) => c !== undefined);
.map((id: string) => data.characters.find((c: CharacterWithRelations) => c.id === id))
.filter((c: CharacterWithRelations | undefined): c is CharacterWithRelations => !!c);
}
} catch (e) {
console.error('Failed to parse stored history', e);
@@ -51,9 +126,17 @@
localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId);
}
syncHintAvailability(0, selectedCharacters.length);
isLoaded = true;
});
onDestroy(() => {
clearUnlockTimeout(originUnlockTimeout);
clearUnlockTimeout(fruitUnlockTimeout);
clearUnlockTimeout(affiliationUnlockTimeout);
});
// Save to localStorage whenever selectedCharacters changes (only store IDs)
$: if (isLoaded && selectedCharacters) {
const ids = selectedCharacters.map(char => char.id);
@@ -66,42 +149,19 @@
$: columnVisibility = data.columnVisibility || {};
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
// 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;
function handleCharacterSelect(character: CharacterWithRelations) {
selectCharacter(character);
}
function selectCharacter(character: any) {
function selectCharacter(character: CharacterWithRelations) {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [character, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
// Check if player won
if (character.id === dailyCharacter.id) {
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
// Send request to record win in database
fetch('/daily', {
method: 'POST',
@@ -110,7 +170,8 @@
},
body: JSON.stringify({
characterId: dailyCharacter.id,
tryCount: selectedCharacters.length
tryCount: selectedCharacters.length,
triedCharacterIds
})
}).catch(err => console.error('Failed to record win:', err));
@@ -122,13 +183,15 @@
}
function resetHistory() {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [];
syncHintAvailability(previousGuessCount, 0);
localStorage.removeItem('dailyCharacterHistory');
}
</script>
<svelte:head>
<title>OnePieceDle - Mode du jour</title>
<title>{$t.game.daily.metaTitle}</title>
<style>
@keyframes shadow-pulse {
0% {
@@ -198,7 +261,7 @@
<main
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? '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 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 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">
@@ -206,10 +269,10 @@
<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">
Personnage du jour
{$t.game.daily.title}
</h1>
<p class="mt-2 text-sm text-amber-300">
{data.winCount} {data.winCount > 1 ? 'personnes' : 'personne'} {data.winCount > 1 ? 'ont' : 'a'} trouvé aujourd'hui 🎉
{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}
@@ -217,12 +280,12 @@
class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
onclick={resetHistory}
>
Recommencer
{$t.game.daily.reset}
</button>
{/if}
</div>
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
Devine le personnage. Chaque indice se débloque après un certain nombre de tentatives. Bonne chance !
{$t.game.daily.description}
</p>
</header>
@@ -247,11 +310,16 @@
<CharacterSearchInput
{characters}
{selectedCharacters}
on:select={handleCharacterSelect}
onSelect={handleCharacterSelect}
/>
{/if}
</section>
{#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0}
<FriendsTodaySection friendsTodayResults={data.friendsTodayResults} />
{/if}
<GuessHistoryTable
{selectedCharacters}
{dailyCharacter}

View File

@@ -7,7 +7,10 @@ import { getDateKey } from '$lib/server/daily-character';
export async function POST({ request, locals }) {
try {
const { characterId, tryCount } = await request.json();
const { characterId, tryCount, triedCharacterIds } = await request.json();
const normalizedTriedCharacterIds = Array.isArray(triedCharacterIds)
? triedCharacterIds.filter((id): id is string => typeof id === 'string')
: [];
if (!characterId) {
return json({ error: 'Missing characterId' }, { status: 400 });
@@ -51,7 +54,8 @@ export async function POST({ request, locals }) {
await db.insert(userCharacterHistory).values({
userId: locals.user.id,
characterHistoryId: todayHistoryEntry.id,
tryCount: tryCount
tryCount: tryCount,
triedCharacterIds: normalizedTriedCharacterIds
});
}
} else {

View File

@@ -4,7 +4,7 @@ import { getAllCharacters } from '$lib/server/daily-character';
import { like } from 'drizzle-orm';
export async function load() {
let characters = await getAllCharacters();
const characters = await getAllCharacters();
// Load column visibility config
const columnConfig = await db

View File

@@ -1,28 +1,25 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.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;
let selectedCharacters: any[] = [];
let currentCharacter: any = null;
let selectedCharacters: CharacterWithRelations[] = [];
let currentCharacter: CharacterWithRelations | null = null;
let isLoaded = false;
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> = {};
const columnDisplayNames: Record<string, string> = {
status: 'Statut',
gender: 'Genre',
affiliations: 'Affiliations',
devilFruitType: 'Fruit',
haki: 'Haki',
bounty: 'Prime',
height: 'Taille',
origin: 'Origine',
arc: 'Arc'
};
let columnDisplayNames: Record<string, string> = {};
// Character filters
let characterFilters = {
@@ -31,17 +28,90 @@
hasDevilFruit: null as boolean | null, // null = all, true = with fruit, false = without fruit
status: [] as string[],
hasHeight: false,
hasAge: false,
hasOrigin: false,
arcs: [] as string[]
};
let wasOriginAvailable = false;
let wasFruitAvailable = false;
let wasAffiliationAvailable = false;
let showOriginUnlock = false;
let showFruitUnlock = 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
onMount(() => {
@@ -56,6 +126,7 @@
try {
columnVisibility = JSON.parse(storedColumnVisibility);
} catch (e) {
console.error('Failed to parse column visibility', e);
columnVisibility = data.columnVisibility || {};
}
} else {
@@ -71,6 +142,9 @@
if (!characterFilters.arcs) {
characterFilters.arcs = [];
}
if (typeof characterFilters.hasAge !== 'boolean') {
characterFilters.hasAge = false;
}
} catch (e) {
console.error('Failed to parse filters', e);
}
@@ -86,18 +160,19 @@
const historyIds = JSON.parse(storedHistoryIds);
// Find the character object by ID
currentCharacter = characters.find((c: any) => c.id === charId);
currentCharacter = characters.find((c: CharacterWithRelations) => c.id === charId) || null;
// Find all character objects by their IDs
selectedCharacters = historyIds
.map((id: string) => characters.find((c: any) => c.id === id))
.filter((c: any) => c !== undefined);
.map((id: string) => characters.find((c: CharacterWithRelations) => c.id === id))
.filter((c: CharacterWithRelations | undefined) => !!c) as CharacterWithRelations[];
// If character not found, generate a new one
if (!currentCharacter) {
generateNewCharacter();
}
} catch (e) {
console.error('Failed to parse character data', e);
// If parsing fails, generate a new character
generateNewCharacter();
}
@@ -105,9 +180,16 @@
generateNewCharacter();
}
syncHintAvailability(0, selectedCharacters.length);
isLoaded = true;
});
onDestroy(() => {
clearUnlockTimeout(originUnlockTimeout);
clearUnlockTimeout(fruitUnlockTimeout);
clearUnlockTimeout(affiliationUnlockTimeout);
});
// Save score to localStorage whenever it changes
$: if (isLoaded) {
localStorage.setItem('infiniteScore', score.toString());
@@ -130,26 +212,46 @@
// Save selected character IDs to localStorage whenever it changes
$: if (isLoaded) {
const selectedIds = selectedCharacters.map((c: any) => c.id);
const selectedIds = selectedCharacters.map((c: CharacterWithRelations) => c.id);
localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds));
}
$: allCharacters = data.characters || [];
$: isFrench = $language === 'fr';
function getDisplayArcName(character: CharacterWithRelations, useFrench: boolean): string | null {
if (useFrench && typeof character.frArcName === 'string' && character.frArcName.length > 0) {
return character.frArcName;
}
return character.arcName;
}
// Extract unique arcs from all characters
$: availableArcs = [
...new Map(
$: {
const useFrench = isFrench;
const arcMap = new Map<string, ArcFilterOption>(
allCharacters
.filter((char: any) => char.arcId && char.arcName)
.map((char: any) => [char.arcId, { id: char.arcId, name: char.arcName }])
).values()
]
.sort((a: any, b: any) => (a.name || '').localeCompare(b.name || ''));
.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: any) => {
$: characters = allCharacters.filter((char: CharacterWithRelations) => {
// Gender filter
if (characterFilters.gender.length > 0 && !characterFilters.gender.includes(char.gender)) {
if (characterFilters.gender.length > 0 && (char.gender == null || !characterFilters.gender.includes(char.gender))) {
return false;
}
@@ -179,67 +281,68 @@
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 && !characterFilters.arcs.includes(char.arcId)) {
if (characterFilters.arcs.length > 0 && (char.arcId == null || !characterFilters.arcs.includes(char.arcId))) {
return false;
}
return true;
});
$: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id);
$: {
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;
}
// 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;
}
$: 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() {
if (characters.length === 0) return;
currentCharacter = characters[Math.floor(Math.random() * characters.length)];
syncHintAvailability(selectedCharacters.length, 0);
selectedCharacters = [];
}
function handleCharacterSelect(event: CustomEvent) {
const character = event.detail;
function handleCharacterSelect(character: CharacterWithRelations) {
selectCharacter(character);
}
function selectCharacter(character: any) {
function selectCharacter(character: CharacterWithRelations) {
const current = currentCharacter;
if (!current) {
return;
}
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [character, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
// Check if player won
if (character.id === currentCharacter.id) {
if (character.id === current.id) {
// Increment score (saved to localStorage via reactive statement)
score++;
// Don't auto-generate next character - wait for user to click "Recommencer"
@@ -265,10 +368,16 @@
}
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) {
@@ -325,6 +434,13 @@
}
}
function toggleAgeFilter() {
characterFilters.hasAge = !characterFilters.hasAge;
if (!hasWon) {
generateNewCharacter();
}
}
function toggleOriginFilter() {
characterFilters.hasOrigin = !characterFilters.hasOrigin;
// Regenerate character with new filters
@@ -352,6 +468,7 @@
hasDevilFruit: null,
status: [],
hasHeight: false,
hasAge: false,
hasOrigin: false,
arcs: []
};
@@ -392,20 +509,26 @@
</script>
<svelte:head>
<title>OnePieceDle - Mode Infini</title>
<title>{$t.game.infinite.metaTitle}</title>
<style>
@keyframes shadow-pulse {
0% {
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
text-shadow:
0 0 20px rgba(0, 0, 0, 0.5),
0 0 40px rgba(50, 50, 50, 0.8);
opacity: 1;
}
50% {
text-shadow: 0 0 60px rgba(0, 0, 0, 0.9), 0 0 100px rgba(30, 30, 30, 1),
text-shadow:
0 0 60px rgba(0, 0, 0, 0.9),
0 0 100px rgba(30, 30, 30, 1),
inset 0 0 50px rgba(0, 0, 0, 0.7);
opacity: 0.9;
}
100% {
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
text-shadow:
0 0 20px rgba(0, 0, 0, 0.5),
0 0 40px rgba(50, 50, 50, 0.8);
opacity: 1;
}
}
@@ -461,34 +584,37 @@
</svelte:head>
<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-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%)]"
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-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)] opacity-20 mix-blend-screen"
></div>
<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 w-full flex-col items-start gap-6">
<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">
Mode Infini
<h1 class="text-3xl font-black tracking-[0.25em] text-amber-50 uppercase sm:text-5xl">
{$t.game.infinite.title}
</h1>
<p class="mt-2 text-2xl font-bold text-amber-300">Score: {score}</p>
<p class="mt-2 text-2xl font-bold text-amber-300">{$t.game.infinite.score}: {score}</p>
</div>
{#if score > 0}
<button
class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
onclick={resetScore}
>
Réinitialiser
{$t.game.infinite.resetScore}
</button>
{/if}
</div>
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
Devine des personnages à l'infini ! Chaque indice se débloque après un certain nombre de
tentatives. Bonne chance !
{$t.game.infinite.description}
</p>
</header>
@@ -496,17 +622,13 @@
{#if currentCharacter}
{#if hasWon}
<div>
<WinPanel
selectedCharacter={currentCharacter}
{selectedCharacters}
{isGeckoMoriaWin}
/>
<WinPanel selectedCharacter={currentCharacter} {selectedCharacters} {isGeckoMoriaWin} />
<button
type="button"
onclick={nextCharacter}
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"
>
Recommencer
{$t.game.infinite.nextCharacter}
</button>
</div>
{:else}
@@ -518,25 +640,27 @@
{showFruitUnlock}
{showAffiliationUnlock}
/>
<div class="flex justify-center mt-2">
<div class="mt-2 flex justify-center">
<button
type="button"
onclick={revealAnswer}
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"
>
Révéler la réponse
{$t.game.infinite.revealAnswer}
</button>
</div>
{/if}
<CharacterSearchInput
{characters}
{selectedCharacters}
on:select={handleCharacterSelect}
onSelect={handleCharacterSelect}
/>
{/if}
{:else}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<p class="text-center text-slate-300">Chargement du personnage...</p>
<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"
>
<p class="text-center text-slate-300">{$t.game.infinite.loadingCharacter}</p>
</div>
{/if}
</section>
@@ -550,16 +674,18 @@
<!-- Character Filters -->
<section class="mt-6">
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 sm:p-4 backdrop-blur">
<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 uppercase tracking-[0.2em] text-amber-200">Filtres de personnages</h3>
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
<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 hover:text-red-200 transition"
class="text-xs text-red-300 transition hover:text-red-200"
>
Réinitialiser
{$t.game.infinite.clearFilters}
</button>
{/if}
</div>
@@ -567,17 +693,19 @@
<div class="space-y-3">
<!-- Gender Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Genre</p>
<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}
{#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)
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' ? 'Homme' : 'Femme'}
{gender === 'Male' ? $t.game.infinite.male : $t.game.infinite.female}
</button>
{/each}
</div>
@@ -585,17 +713,19 @@
<!-- Status Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Statut</p>
<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}
{#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)
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' ? 'Vivant' : status === 'Dead' ? 'Mort' : 'Inconnu'}
{status === 'Alive' ? $t.game.infinite.alive : status === 'Dead' ? $t.game.infinite.dead : $t.game.infinite.unknown}
</button>
{/each}
</div>
@@ -603,7 +733,7 @@
<!-- Haki Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Capacités</p>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterAbilities}</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
@@ -612,25 +742,30 @@
? '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'}"
>
A du Haki
{$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
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'}"
? '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 ? 'Fruit (Tous)' : characterFilters.hasDevilFruit ? 'Avec Fruit' : 'Sans Fruit'}
{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="text-xs text-slate-400 mb-2">Informations</p>
<p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterInformation}</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
@@ -639,7 +774,16 @@
? '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'}"
>
Taille définie
{$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"
@@ -648,20 +792,22 @@
? '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'}"
>
Origine définie
{$t.game.infinite.originDefined}
</button>
</div>
</div>
<!-- Arc Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Arcs</p>
<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)
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'}"
>
@@ -671,8 +817,8 @@
</div>
</div>
<p class="text-xs text-slate-500 mt-2">
{characters.length} personnage{characters.length > 1 ? 's' : ''} disponible{characters.length > 1 ? 's' : ''}
<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>
@@ -680,11 +826,15 @@
<!-- Column Visibility Toggle -->
<section class="mt-6">
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 sm:p-4 backdrop-blur">
<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 uppercase tracking-[0.2em] text-amber-200">Colonnes</h3>
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
{$t.game.infinite.columnsTitle}
</h3>
<p class="text-xs text-slate-400">
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(columnVisibility).length}
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(
columnVisibility
).length}
</p>
</div>
<div class="flex flex-wrap gap-2">

View File

@@ -2,7 +2,10 @@ import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import type { PageServerLoad } from './$types';
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 { sql } from 'drizzle-orm';
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
@@ -14,9 +17,28 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = {
signInEmail: async (event) => {
const formData = await event.request.formData();
const email = formData.get('email')?.toString() ?? '';
const identifier = formData.get('identifier')?.toString().trim() ?? formData.get('email')?.toString().trim() ?? '';
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 {
await auth.api.signInEmail({
body: {
@@ -38,7 +60,33 @@ export const actions: Actions = {
const formData = await event.request.formData();
const email = formData.get('email')?.toString() ?? '';
const password = formData.get('password')?.toString() ?? '';
const confirmPassword = formData.get('confirmPassword')?.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 {
await auth.api.signUpEmail({
@@ -46,6 +94,7 @@ export const actions: Actions = {
email,
password,
name,
username,
callbackURL: '/auth/verification-success'
}
});

View File

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

View File

@@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from './$types';
import { auth } from '$lib/server/auth';
import { db } from '$lib/server/db';
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
import { and, desc, eq, or } from 'drizzle-orm';
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm';
import { APIError } from 'better-auth/api';
export const load: PageServerLoad = async (event) => {
@@ -20,12 +20,13 @@ export const load: PageServerLoad = async (event) => {
.where(eq(session.userId, event.locals.user.id));
// Fetch daily history for this user
const dailyHistory = await db
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
@@ -36,13 +37,37 @@ export const load: PageServerLoad = async (event) => {
.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,
requesterEmail: user.email,
requesterUsername: user.username,
requesterImage: user.image
})
.from(friendship)
@@ -56,7 +81,7 @@ export const load: PageServerLoad = async (event) => {
createdAt: friendship.createdAt,
addresseeId: friendship.addresseeId,
addresseeName: user.name,
addresseeEmail: user.email,
addresseeUsername: user.username,
addresseeImage: user.image
})
.from(friendship)
@@ -70,7 +95,7 @@ export const load: PageServerLoad = async (event) => {
createdAt: friendship.createdAt,
friendId: friendship.addresseeId,
friendName: user.name,
friendEmail: user.email,
friendUsername: user.username,
friendImage: user.image
})
.from(friendship)
@@ -83,7 +108,7 @@ export const load: PageServerLoad = async (event) => {
createdAt: friendship.createdAt,
friendId: friendship.requesterId,
friendName: user.name,
friendEmail: user.email,
friendUsername: user.username,
friendImage: user.image
})
.from(friendship)
@@ -190,6 +215,7 @@ export const actions: Actions = {
// Delete the session from database
await db.delete(session).where(eq(session.id, sessionId));
} catch (error) {
console.error('Error revoking session:', error);
return fail(500, { message: 'Erreur lors de la révocation de la session' });
}
@@ -201,25 +227,27 @@ export const actions: Actions = {
}
const formData = await event.request.formData();
const friendEmail = formData.get('friendEmail')?.toString().trim().toLowerCase() ?? '';
const friendUsername = formData.get('friendUsername')?.toString().trim() ?? '';
if (!friendEmail) {
return fail(400, { message: 'Email requis pour envoyer une demande' });
if (!friendUsername) {
return fail(400, { message: 'Nom d\'utilisateur requis pour envoyer une demande' });
}
const me = event.locals.user;
if (friendEmail === me.email?.toLowerCase()) {
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, email: user.email })
.select({ id: user.id, username: user.username })
.from(user)
.where(eq(user.email, friendEmail))
.where(sql`lower(${user.username}) = ${friendUsername.toLowerCase()}`)
.limit(1);
if (!targetUser) {
return fail(404, { message: 'Aucun utilisateur trouvé avec cet email' });
return fail(404, { message: 'Aucun utilisateur trouvé avec ce nom d\'utilisateur' });
}
const [existing] = await db

View File

@@ -1,51 +1,56 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
import { resolve } from '$app/paths';
import { t, language } from '$lib/i18n';
interface Props {
data: PageData;
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 isLoading = $state(false);
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
let name = $state('');
let friendEmail = $state('');
let name = $derived(data.user?.name || '');
let friendUsername = $state('');
let showSuccess = $state(false);
let oldPassword = $state('');
let newPassword = $state('');
let confirmPassword = $state('');
let sessions = $state<any[]>([]);
let dailyHistory = $state<any[]>([]);
let friends = $state<any[]>([]);
let incomingRequests = $state<any[]>([]);
let outgoingRequests = $state<any[]>([]);
let sessions = $derived(data.sessions || []);
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;
$effect(() => {
name = data.user?.name || '';
friends = data.friends || [];
});
$effect(() => {
sessions = (data as any).sessions || [];
incomingRequests = data.incomingRequests || [];
});
$effect(() => {
dailyHistory = (data as any).dailyHistory || [];
});
$effect(() => {
friends = (data as any).friends || [];
});
$effect(() => {
incomingRequests = (data as any).incomingRequests || [];
});
$effect(() => {
outgoingRequests = (data as any).outgoingRequests || [];
outgoingRequests = data.outgoingRequests || [];
});
$effect(() => {
@@ -67,11 +72,11 @@
</script>
<svelte:head>
<title>Mon Profil - OnePieceDle</title>
<title>{$t.game.profile.pageTitle} - OnePieceDle</title>
</svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100">
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
<div class="absolute inset-0 bg-linear-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="relative mx-auto flex w-full max-w-2xl flex-col items-center px-6 py-4">
@@ -79,10 +84,10 @@
<!-- Header -->
<div class="text-center">
<h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl">
Mon Profil
{$t.game.profile.headerTitle}
</h1>
<p class="mt-2 text-sm text-slate-300">
Modifie les informations de ton profil
{$t.game.profile.headerSubtitle}
</p>
</div>
@@ -90,43 +95,43 @@
<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
onclick={() => handleTabChange('profile')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'profile'
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'profile'
? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}"
>
Profil
{$t.game.profile.tabProfile}
</button>
<button
onclick={() => handleTabChange('password')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'password'
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'password'
? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}"
>
Mot de passe
{$t.game.profile.tabPassword}
</button>
<button
onclick={() => handleTabChange('daily')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === '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'}"
>
Historique Daily
{$t.game.profile.tabDaily}
</button>
<button
onclick={() => handleTabChange('sessions')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'sessions'
class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'sessions'
? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}"
>
Sessions
{$t.game.profile.tabSessions}
</button>
<button
onclick={() => handleTabChange('friends')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === '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'}"
>
Amis
{$t.game.profile.tabFriends}
</button>
</div>
@@ -138,7 +143,7 @@
{#if data.user.image}
<img
src={data.user.image}
alt={data.user.name || 'Profil'}
alt={data.user.name || $t.game.profile.avatarFallbackAlt}
class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover"
/>
{:else}
@@ -147,7 +152,7 @@
</div>
{/if}
<div class="text-center">
<p class="text-sm text-slate-400">Email</p>
<p class="text-sm text-slate-400">{$t.game.profile.email}</p>
<p class="font-semibold text-white">{data.user.email}</p>
</div>
</div>
@@ -169,7 +174,7 @@
<!-- Name Field -->
<div>
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nom d'affichage
{$t.game.profile.displayName}
</label>
<input
id="name"
@@ -177,7 +182,7 @@
name="name"
bind:value={name}
required
placeholder="Ton nom"
placeholder={$t.game.profile.displayNamePlaceholder}
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>
@@ -192,7 +197,7 @@
<!-- Success Message -->
{#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
Profil mis à jour avec succès !
{$t.game.profile.profileUpdateSuccess}
</div>
{/if}
@@ -202,7 +207,7 @@
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"
>
{isLoading ? 'Mise à jour...' : 'Enregistrer les modifications'}
{isLoading ? $t.game.profile.updating : $t.game.profile.saveChanges}
</button>
</form>
</div>
@@ -212,7 +217,7 @@
{#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">
Système d'amis
{$t.game.profile.friendsTitle}
</h2>
<form
@@ -222,23 +227,23 @@
isLoading = true;
return async ({ update }) => {
isLoading = false;
friendEmail = '';
friendUsername = '';
await update();
};
}}
class="mb-8 space-y-3"
>
<label for="friendEmail" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Ajouter un ami par email
<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="friendEmail"
type="email"
name="friendEmail"
id="friendUsername"
type="text"
name="friendUsername"
required
bind:value={friendEmail}
placeholder="ami@email.com"
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
@@ -246,7 +251,7 @@
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 ? 'Envoi...' : 'Envoyer'}
{isLoading ? $t.game.profile.sending : $t.game.profile.send}
</button>
</div>
{#if form?.message}
@@ -256,25 +261,25 @@
<div class="space-y-8">
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Demandes reçues</h3>
<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">Aucune demande reçue.</p>
<p class="text-sm text-slate-400">{$t.game.profile.noIncomingRequests}</p>
{:else}
<div class="space-y-3">
{#each incomingRequests as req}
{#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.requesterEmail}</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">Accepter</button>
<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">Refuser</button>
<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>
@@ -284,20 +289,20 @@
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Demandes envoyées</h3>
<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">Aucune demande envoyée.</p>
<p class="text-sm text-slate-400">{$t.game.profile.noOutgoingRequests}</p>
{:else}
<div class="space-y-3">
{#each outgoingRequests as req}
{#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.addresseeEmail}</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">Annuler</button>
<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}
@@ -306,20 +311,20 @@
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Mes amis</h3>
<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">Tu n'as pas encore d'amis.</p>
<p class="text-sm text-slate-400">{$t.game.profile.noFriends}</p>
{:else}
<div class="space-y-3">
{#each friends as friend}
{#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.friendEmail}</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">Supprimer</button>
<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}
@@ -334,7 +339,7 @@
{#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">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Changer le mot de passe
{$t.game.profile.changePasswordTitle}
</h2>
<!-- Form -->
@@ -356,7 +361,7 @@
<!-- Old Password Field -->
<div>
<label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Mot de passe actuel
{$t.game.profile.currentPassword}
</label>
<input
id="oldPassword"
@@ -372,7 +377,7 @@
<!-- New Password Field -->
<div>
<label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nouveau mot de passe
{$t.game.profile.newPassword}
</label>
<input
id="newPassword"
@@ -388,7 +393,7 @@
<!-- Confirm Password Field -->
<div>
<label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Confirmer le mot de passe
{$t.game.profile.confirmPassword}
</label>
<input
id="confirmPassword"
@@ -411,7 +416,7 @@
<!-- Success Message -->
{#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
Mot de passe changé avec succès !
{$t.game.profile.passwordChangeSuccess}
</div>
{/if}
@@ -421,7 +426,7 @@
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"
>
{isLoading ? 'Changement en cours...' : 'Changer le mot de passe'}
{isLoading ? $t.game.profile.changing : $t.game.profile.changePassword}
</button>
</form>
</div>
@@ -431,17 +436,17 @@
{#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">
Historique des Daily
{$t.game.profile.dailyHistoryTitle}
</h2>
{#if dailyHistory.length === 0}
<p class="text-center text-slate-400">Aucun historique disponible</p>
<p class="text-center text-slate-400">{$t.game.profile.noDailyHistory}</p>
{:else}
<div class="space-y-4">
{#each dailyHistory as day}
{#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="flex-shrink-0">
<div class="shrink-0">
{#if day.characterImage}
<img
src={day.characterImage}
@@ -450,7 +455,7 @@
/>
{: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">N/A</span>
<span class="text-xs text-slate-400">{$t.game.profile.noImage}</span>
</div>
{/if}
</div>
@@ -459,18 +464,41 @@
<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('fr-FR', {
{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 ? 'tentative' : 'tentatives'}
{day.tryCount} {day.tryCount === 1 ? $t.game.profile.trySingular : $t.game.profile.tryPlural}
</p>
</div>
</div>
@@ -484,24 +512,24 @@
{#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">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Sessions actives
{$t.game.profile.activeSessionsTitle}
</h2>
{#if sessions.length === 0}
<p class="text-center text-slate-400">Aucune session active</p>
<p class="text-center text-slate-400">{$t.game.profile.noActiveSessions}</p>
{:else}
<div class="space-y-4">
{#each sessions as sess}
{#each sessions as sess (sess.id)}
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4">
<div class="flex-1">
<p class="font-semibold text-white">
{sess.userAgent || 'Appareil inconnu'}
{sess.userAgent || $t.game.profile.unknownDevice}
</p>
<p class="text-xs text-slate-400">
IP: {sess.ipAddress || 'Inconnue'}
{$t.game.profile.ip}: {sess.ipAddress || $t.game.profile.unknown}
</p>
<p class="mt-1 text-xs text-slate-500">
Créée: {new Date(sess.createdAt).toLocaleDateString('fr-FR', {
{$t.game.profile.created}: {new Date(sess.createdAt).toLocaleDateString($language === 'fr' ? 'fr-FR' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -525,7 +553,7 @@
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"
>
Terminer
{$t.game.profile.terminate}
</button>
</form>
</div>
@@ -537,8 +565,8 @@
<!-- Back to Home -->
<div class="text-center">
<a href="/" class="text-sm text-slate-400 transition hover:text-slate-300">
Retour à l'accueil
<a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300">
{$t.game.profile.backHome}
</a>
</div>
</div>