Compare commits
33 Commits
a91b298ee5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef6bf9862e | |||
| d75c74ac3c | |||
| fa14156d82 | |||
| 29297d3773 | |||
| 28bb8f526b | |||
| 288271fb04 | |||
| fb64c84a17 | |||
| 81e205dd4e | |||
| ded1c8313d | |||
| 4426b5d28a | |||
| 5ad0428420 | |||
| 7760570365 | |||
| 5fde54a2a7 | |||
| 2a3c82f777 | |||
| 835163f5bb | |||
| 5020393b22 | |||
| 94393851c8 | |||
| 997b2f1781 | |||
| bd121b7d85 | |||
| 6d2dccd47f | |||
| 5fdde9d177 | |||
| b1cc691422 | |||
| 8b08950719 | |||
| fd83ac911a | |||
| eeccf812cf | |||
| 9485d9841c | |||
| 31308ef126 | |||
| 57a0427e77 | |||
| 3bd2506c2f | |||
| e5a21cb0af | |||
| 4e95abf09f | |||
| 66afda5101 | |||
| a041a8caf5 |
@@ -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`);
|
||||
199
drizzle/0000_huge_doctor_octopus.sql
Normal file
199
drizzle/0000_huge_doctor_octopus.sql
Normal 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`);
|
||||
1
drizzle/0001_fuzzy_talisman.sql
Normal file
1
drizzle/0001_fuzzy_talisman.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `character_scrape_validation` ADD `is_deleted` integer DEFAULT false;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `user` ADD `is_admin` integer DEFAULT false NOT NULL;
|
||||
1
drizzle/0002_old_earthquake.sql
Normal file
1
drizzle/0002_old_earthquake.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE `character_override`;
|
||||
8
drizzle/0003_mixed_ben_grimm.sql
Normal file
8
drizzle/0003_mixed_ben_grimm.sql
Normal 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`;
|
||||
@@ -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`);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
CREATE UNIQUE INDEX `userCharacterHistory_userId_characterHistoryId_unique` ON `userCharacterHistory` (`userId`,`characterHistoryId`);
|
||||
@@ -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;
|
||||
@@ -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`);
|
||||
@@ -1,51 +0,0 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
ALTER TABLE `user` ADD `username` text;--> statement-breakpoint
|
||||
UPDATE `user`
|
||||
SET `username` = `name`
|
||||
WHERE `username` IS NULL OR trim(`username`) = '';--> statement-breakpoint
|
||||
UPDATE `user`
|
||||
SET `username` = `username` || '_' || substr(`id`, 1, 6)
|
||||
WHERE `id` IN (
|
||||
SELECT u1.`id`
|
||||
FROM `user` u1
|
||||
JOIN `user` u2
|
||||
ON lower(u1.`username`) = lower(u2.`username`)
|
||||
AND u1.`id` > u2.`id`
|
||||
);--> statement-breakpoint
|
||||
CREATE TABLE `__new_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
|
||||
INSERT INTO `__new_user` (
|
||||
`id`,
|
||||
`name`,
|
||||
`username`,
|
||||
`email`,
|
||||
`email_verified`,
|
||||
`image`,
|
||||
`is_admin`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
)
|
||||
SELECT
|
||||
`id`,
|
||||
`name`,
|
||||
`username`,
|
||||
`email`,
|
||||
`email_verified`,
|
||||
`image`,
|
||||
`is_admin`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
FROM `user`;--> statement-breakpoint
|
||||
DROP TABLE `user`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -5,71 +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",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1772822823122,
|
||||
"tag": "0009_true_gravity",
|
||||
"when": 1776195681488,
|
||||
"tag": "0003_mixed_ben_grimm",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,50 +1,65 @@
|
||||
[
|
||||
"aladdin_aladdin",
|
||||
"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",
|
||||
"baggy_baggy",
|
||||
"buggy_buggy",
|
||||
"bartholomew_kuma_bartholomew_kuma",
|
||||
"bartolomeo_bartolomeo",
|
||||
"basil_hawkins_basil_hawkins",
|
||||
"batman_batman",
|
||||
"bell-mère_bell-mère",
|
||||
"bellamy_bellamy",
|
||||
"belo_betty_belo_betty",
|
||||
"ben_beckman_ben_beckman",
|
||||
"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",
|
||||
"cesar_clown_cesar_clown",
|
||||
"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",
|
||||
"coby_coby",
|
||||
"corazon_corazon",
|
||||
"clou_d_clover_clou_d_clover",
|
||||
"crocodile_crocodile",
|
||||
"crocus_crocus",
|
||||
"curly_dadan_curly_dadan",
|
||||
"dalton_dalton",
|
||||
"daz_bones_daz_bones",
|
||||
"daz_bonez_daz_bonez",
|
||||
"denjiro_denjiro",
|
||||
"diamante_diamante",
|
||||
"doc_q_doc_q",
|
||||
"don_quichotte_doflamingo_don_quichotte_doflamingo",
|
||||
"don_quichotte_rossinante_don_quichotte_rossinante",
|
||||
"donquixote_doflamingo_donquixote_doflamingo",
|
||||
"donquixote_rosinante_donquixote_rosinante",
|
||||
"dorry_dorry",
|
||||
"dracule_mihawk_dracule_mihawk",
|
||||
"duval_duval",
|
||||
"vegapunk/edison_edison",
|
||||
"edward_newgate_edward_newgate",
|
||||
"edward_weevil_edward_weevil",
|
||||
"emporio_ivankov_emporio_ivankov",
|
||||
@@ -53,30 +68,47 @@
|
||||
"fisher_tiger_fisher_tiger",
|
||||
"foxy_foxy",
|
||||
"franky_franky",
|
||||
"fujitora_fujitora",
|
||||
"gan_forr_gan_forr",
|
||||
"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",
|
||||
"haguar_d_sauro_haguar_d_sauro",
|
||||
"guernika_guernika",
|
||||
"hack_hack",
|
||||
"jaguar_d_saul_jaguar_d_saul",
|
||||
"hajrudin_hajrudin",
|
||||
"hannyabal_hannyabal",
|
||||
"hatchan_hatchan",
|
||||
"harald_harald",
|
||||
"haredas_haredas",
|
||||
"heracles_heracles",
|
||||
"helmeppo_helmeppo",
|
||||
"hibari_hibari",
|
||||
"hiriluk_hiriluk",
|
||||
"hina_hina",
|
||||
"hody_jones_hody_jones",
|
||||
"hyogoro_hyogoro",
|
||||
"hogback_hogback",
|
||||
"hyougoro_hyougoro",
|
||||
"iceburg_iceburg",
|
||||
"igaram_igaram",
|
||||
"imu_imu",
|
||||
"inazuma_inazuma",
|
||||
"inuarashi_inuarashi",
|
||||
"issho_issho",
|
||||
"izo_izo",
|
||||
"izou_izou",
|
||||
"jabra_jabra",
|
||||
"jack_jack",
|
||||
"jango_jango",
|
||||
"jesus_burgess_jesus_burgess",
|
||||
"jewelry_bonney_jewelry_bonney",
|
||||
"jinbei_jinbei",
|
||||
"jinbe_jinbe",
|
||||
"giolla_giolla",
|
||||
"joy_boy_joy_boy",
|
||||
"jozu_jozu",
|
||||
"kaidou_kaidou",
|
||||
"kaku_kaku",
|
||||
"kalgara_kalgara",
|
||||
@@ -85,60 +117,139 @@
|
||||
"karoo_karoo",
|
||||
"kawamatsu_kawamatsu",
|
||||
"kaya_kaya",
|
||||
"kelly_funk_kelly_funk",
|
||||
"kikunojo_kikunojo",
|
||||
"killer_killer",
|
||||
"kinemon_kinemon",
|
||||
"kin'emon_kin'emon",
|
||||
"koala_koala",
|
||||
"koby_koby",
|
||||
"kong_kong",
|
||||
"kozuki_hiyori_kozuki_hiyori",
|
||||
"kozuki_momonosuke_kozuki_momonosuke",
|
||||
"kozuki_oden_kozuki_oden",
|
||||
"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",
|
||||
"montblanc_norland_montblanc_norland",
|
||||
"mont_blanc_cricket_mont_blanc_cricket",
|
||||
"mont_blanc_noland_mont_blanc_noland",
|
||||
"morgans_morgans",
|
||||
"morgan_morgan",
|
||||
"morley_morley",
|
||||
"mr_3_mr_3",
|
||||
"nami_nami",
|
||||
"nefertari_cobra_nefertari_cobra",
|
||||
"nefertari_vivi_nefertari_vivi",
|
||||
"nekomamushi_nekomamushi",
|
||||
"neptune_neptune",
|
||||
"nico_olvia_nico_olvia",
|
||||
"nico_robin_nico_robin",
|
||||
"oars_oars",
|
||||
"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",
|
||||
"yamato_yamato"
|
||||
"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"
|
||||
]
|
||||
@@ -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!`);
|
||||
|
||||
@@ -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
@@ -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}:`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
if (Array.isArray(parsedEpithets)) {
|
||||
epithetsMatches = parsedEpithets.some((epithet: string) =>
|
||||
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)
|
||||
);
|
||||
} 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);
|
||||
!selectedCharacters.some((selected) => selected.id === char.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Reset highlighted index when filtered list changes
|
||||
$: if (filteredCharacters) {
|
||||
highlightedIndex = 0;
|
||||
// Reset highlighted index when filtered list changes.
|
||||
$effect(() => {
|
||||
const nextFilteredCharacters = filteredCharacters;
|
||||
if (!nextFilteredCharacters) {
|
||||
return;
|
||||
}
|
||||
state.highlightedIndex = 0;
|
||||
});
|
||||
|
||||
// Scroll highlighted item into view.
|
||||
$effect(() => {
|
||||
const nextFilteredCharacters = filteredCharacters;
|
||||
|
||||
if (!state.dropdownContainer || state.highlightedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
if (state.highlightedIndex >= nextFilteredCharacters.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
function selectCharacter(character: any) {
|
||||
dispatch('select', character);
|
||||
searchInput = '';
|
||||
highlightedIndex = 0;
|
||||
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,17 +198,12 @@
|
||||
</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>
|
||||
{/each}
|
||||
@@ -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>
|
||||
|
||||
73
src/lib/components/FriendsTodaySection.svelte
Normal file
73
src/lib/components/FriendsTodaySection.svelte
Normal 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}
|
||||
@@ -1,192 +1,344 @@
|
||||
<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
|
||||
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
|
||||
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>
|
||||
{#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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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}
|
||||
href={getWikiBaseUrl() + getWikiUrl(character)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="block w-full h-full"
|
||||
class="block h-full w-full"
|
||||
>
|
||||
<img
|
||||
src={character.pictureUrl}
|
||||
alt={character.name}
|
||||
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
|
||||
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">
|
||||
<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'
|
||||
? 'Vivant'
|
||||
: character.status === 'Deceased' || character.status === 'Dead'
|
||||
? 'Mort'
|
||||
? $t.game.components.guessHistory.alive
|
||||
: character.status === 'Dead'
|
||||
? $t.game.components.guessHistory.dead
|
||||
: character.status === 'Unknown'
|
||||
? 'Inconnu'
|
||||
? $t.game.components.guessHistory.unknown
|
||||
: character.status === null
|
||||
? '-'
|
||||
: character.status || 'Inconnu'}
|
||||
: 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'}
|
||||
<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}
|
||||
{#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">
|
||||
<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-[10px] sm:text-xs md:text-sm font-bold text-white text-center">{character.devilFruitType}</p>
|
||||
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
|
||||
{character.devilFruitType}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-2xl sm:text-3xl md:text-5xl font-bold text-white text-center">✕</p>
|
||||
<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) {
|
||||
<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) ||
|
||||
} else if (
|
||||
(character.hakiObservation && dailyCharacter.hakiObservation) ||
|
||||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
|
||||
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
|
||||
(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}
|
||||
})()} 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}
|
||||
@@ -196,61 +348,145 @@
|
||||
|
||||
<!-- 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">
|
||||
<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="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||
<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>
|
||||
"
|
||||
></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>
|
||||
<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="text-[10px] sm:text-xs md:text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
||||
<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">
|
||||
<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="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||
<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>
|
||||
"
|
||||
></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>
|
||||
<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>
|
||||
<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
|
||||
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>
|
||||
"
|
||||
></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>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
104
src/lib/components/LanguageSwitcher.svelte
Normal file
104
src/lib/components/LanguageSwitcher.svelte
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{#if isFrench}
|
||||
<a
|
||||
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
||||
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"
|
||||
>
|
||||
Voir la page
|
||||
{$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
233
src/lib/i18n/en.json
Normal 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
233
src/lib/i18n/fr.json
Normal 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
51
src/lib/i18n/index.ts
Normal 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);
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,61 +1,8 @@
|
||||
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';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const [charactersData, devilFruits, arcs, overrides, statusesData, gendersData] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
gender: character.gender,
|
||||
age: character.age,
|
||||
affiliations: character.affiliations,
|
||||
devilFruitId: character.devilFruitId,
|
||||
hakiObservation: character.hakiObservation,
|
||||
hakiArmament: character.hakiArmament,
|
||||
hakiConqueror: character.hakiConqueror,
|
||||
bounty: character.bounty,
|
||||
height: character.height,
|
||||
origin: character.origin,
|
||||
firstAppearance: character.firstAppearance,
|
||||
pictureUrl: character.pictureUrl,
|
||||
epithets: character.epithets,
|
||||
status: character.status,
|
||||
url: character.url,
|
||||
arcId: character.arcId,
|
||||
isInDailyMode: character.isInDailyMode,
|
||||
arcName: arc.name,
|
||||
devilFruitName: devilFruit.name,
|
||||
devilFruitType: devilFruit.type
|
||||
})
|
||||
.from(character)
|
||||
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||
.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} != ''`),
|
||||
db.selectDistinct({ gender: character.gender })
|
||||
.from(character)
|
||||
.where(sql`${character.gender} IS NOT NULL AND ${character.gender} != ''`)
|
||||
]);
|
||||
|
||||
// Create a map of overrides by characterId for easy lookup
|
||||
const overridesMap = new Map(overrides.map((o) => [o.characterId, o]));
|
||||
|
||||
// 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 => {
|
||||
@@ -71,55 +18,54 @@ export const load: PageServerLoad = async () => {
|
||||
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();
|
||||
export const load: PageServerLoad = async () => {
|
||||
let [characters, devilFruits, arcs, statusesData, gendersData] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
gender: character.gender,
|
||||
age: character.age,
|
||||
affiliation: character.affiliation,
|
||||
devilFruitId: character.devilFruitId,
|
||||
hakiObservation: character.hakiObservation,
|
||||
hakiArmament: character.hakiArmament,
|
||||
hakiConqueror: character.hakiConqueror,
|
||||
bounty: character.bounty,
|
||||
height: character.height,
|
||||
origin: character.origin,
|
||||
firstAppearance: character.firstAppearance,
|
||||
pictureUrl: character.pictureUrl,
|
||||
epithets: normalizeArray(character.epithets),
|
||||
status: character.status,
|
||||
url: character.url,
|
||||
arcId: character.arcId,
|
||||
isInDailyMode: character.isInDailyMode,
|
||||
arcName: arc.name,
|
||||
devilFruitName: devilFruit.name,
|
||||
devilFruitType: devilFruit.type
|
||||
})
|
||||
.from(character)
|
||||
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||
.orderBy(character.name),
|
||||
db.select().from(devilFruit).orderBy(devilFruit.name),
|
||||
db.select().from(arc).orderBy(arc.name),
|
||||
db.selectDistinct({ status: character.status })
|
||||
.from(character)
|
||||
.where(sql`${character.status} IS NOT NULL AND ${character.status} != ''`),
|
||||
db.selectDistinct({ gender: character.gender })
|
||||
.from(character)
|
||||
.where(sql`${character.gender} IS NOT NULL AND ${character.gender} != ''`)
|
||||
]);
|
||||
|
||||
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' });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,11 +9,14 @@
|
||||
<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>
|
||||
<div class="flex items-center gap-3">
|
||||
<LanguageSwitcher />
|
||||
<ProfileButton user={data.user} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-20">
|
||||
{@render children()}
|
||||
|
||||
@@ -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>
|
||||
{#if isFrench}
|
||||
<a
|
||||
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
||||
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"
|
||||
>
|
||||
Voir la page
|
||||
{$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}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
|
||||
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';
|
||||
|
||||
@@ -17,7 +17,13 @@ export async function load(event) {
|
||||
// Load the win count for today
|
||||
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
|
||||
|
||||
let friendsTodayResults: Array<{ userId: string; name: string; image: string | null; tryCount: number }> = [];
|
||||
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;
|
||||
@@ -51,12 +57,13 @@ export async function load(event) {
|
||||
const todayCharacterHistoryId = todayHistoryEntry?.id;
|
||||
|
||||
if (todayCharacterHistoryId) {
|
||||
friendsTodayResults = await db
|
||||
const friendResultsRaw = await db
|
||||
.select({
|
||||
userId: user.id,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
tryCount: userCharacterHistory.tryCount
|
||||
tryCount: userCharacterHistory.tryCount,
|
||||
triedCharacterIds: userCharacterHistory.triedCharacterIds
|
||||
})
|
||||
.from(userCharacterHistory)
|
||||
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
|
||||
@@ -67,6 +74,33 @@ export async function load(event) {
|
||||
)
|
||||
)
|
||||
.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)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,39 +310,14 @@
|
||||
<CharacterSearchInput
|
||||
{characters}
|
||||
{selectedCharacters}
|
||||
on:select={handleCharacterSelect}
|
||||
onSelect={handleCharacterSelect}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
{#if hasWon && data.friendsTodayResults && data.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">Tes amis aujourd'hui</p>
|
||||
<div class="mt-4 space-y-2">
|
||||
{#each data.friendsTodayResults as friendResult}
|
||||
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2">
|
||||
<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 ? 'coups' : 'coup'}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
<FriendsTodaySection friendsTodayResults={data.friendsTodayResults} />
|
||||
{/if}
|
||||
|
||||
<GuessHistoryTable
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}"
|
||||
>
|
||||
{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">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<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;
|
||||
@@ -24,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>
|
||||
@@ -41,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>
|
||||
|
||||
@@ -63,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"
|
||||
@@ -71,7 +73,7 @@
|
||||
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>
|
||||
@@ -81,7 +83,7 @@
|
||||
{#if isSignUp}
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Nom d'utilisateur
|
||||
{$t.game.login.usernameLabel}
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
@@ -89,7 +91,7 @@
|
||||
name="username"
|
||||
bind:value={username}
|
||||
required
|
||||
placeholder="ex: luffy_gear5"
|
||||
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>
|
||||
@@ -98,7 +100,7 @@
|
||||
<!-- Email / Username Field -->
|
||||
<div>
|
||||
<label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
{isSignUp ? 'E-mail' : 'E-mail ou nom d\'utilisateur'}
|
||||
{isSignUp ? $t.game.login.identifierLabelSignUp : $t.game.login.identifierLabelSignIn}
|
||||
</label>
|
||||
<input
|
||||
id="identifier"
|
||||
@@ -106,7 +108,7 @@
|
||||
name={isSignUp ? 'email' : 'identifier'}
|
||||
bind:value={email}
|
||||
required
|
||||
placeholder={isSignUp ? 'votremail@email.com' : 'votremail@email.com ou luffy_gear5'}
|
||||
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>
|
||||
@@ -114,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"
|
||||
@@ -134,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"
|
||||
@@ -161,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>
|
||||
@@ -182,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>
|
||||
|
||||
@@ -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, sql } 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,6 +37,30 @@ 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,
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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
|
||||
@@ -229,7 +234,7 @@
|
||||
class="mb-8 space-y-3"
|
||||
>
|
||||
<label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Ajouter un ami par nom d'utilisateur
|
||||
{$t.game.profile.addFriendByUsername}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
@@ -238,7 +243,7 @@
|
||||
name="friendUsername"
|
||||
required
|
||||
bind:value={friendUsername}
|
||||
placeholder="ex: luffy_gear5"
|
||||
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,12 +261,12 @@
|
||||
|
||||
<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>
|
||||
@@ -270,11 +275,11 @@
|
||||
<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,12 +289,12 @@
|
||||
</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>
|
||||
@@ -297,7 +302,7 @@
|
||||
</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,12 +311,12 @@
|
||||
</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>
|
||||
@@ -319,7 +324,7 @@
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user