Initial commit

This commit is contained in:
WildEgo 2025-06-05 01:08:50 +01:00
commit fe18ec099a
132 changed files with 41418 additions and 0 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
APP_NAME=
APP_URL=
SESSION_KEY=
DATABASE_URL=
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USE_SSL=
EMAIL_USERNAME=
EMAIL_PASSWORD=
EMAIL_FROM=
STORAGE_ENDPOINT=
STORAGE_ACCESS_KEY_ID=
STORAGE_ACCESS_KEY_SECRET=
STORAGE_MALL_BUCKET=
STORAGE_SUPPORT_BUCKET=

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
package-lock.json
yarn.lock
.DS_Store
.cache
.env
.vercel
.output
.vinxi
/build/
/api/
/server/build
/public/build
.vinxi
# Sentry Config File
.env.sentry-build-plugin
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/src/db/seed-data

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"inlang.vs-code-extension"
]
}

17
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome",
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome",
}
}

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# Welcome to TanStack.com!
This site is built with TanStack Router!
- [TanStack Router Docs](https://tanstack.com/router)
It's deployed automagically with Netlify!
- [Netlify](https://netlify.com/)
## Development
From your terminal:
```sh
pnpm install
pnpm dev
```
This starts your app in development mode, rebuilding assets on file changes.
## Editing and previewing the docs of TanStack projects locally
The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app.
In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system.
Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally :
1. Create a new directory called `tanstack`.
```sh
mkdir tanstack
```
2. Enter the directory and clone this repo and the repo of the project there.
```sh
cd tanstack
git clone git@github.com:TanStack/tanstack.com.git
git clone git@github.com:TanStack/form.git
```
> [!NOTE]
> Your `tanstack` directory should look like this:
>
> ```
> tanstack/
> |
> +-- form/
> |
> +-- tanstack.com/
> ```
> [!WARNING]
> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found.
3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode:
```sh
cd tanstack.com
pnpm i
# The app will run on https://localhost:3000 by default
pnpm dev
```
4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`.
> [!NOTE]
> The updated pages need to be manually reloaded in the browser.
> [!WARNING]
> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page!

26
app.config.ts Normal file
View File

@ -0,0 +1,26 @@
import { defineConfig } from '@tanstack/react-start/config';
import tsConfigPaths from 'vite-tsconfig-paths';
import { paraglideVitePlugin as paraglide } from '@inlang/paraglide-js';
export default defineConfig({
tsr: {
appDirectory: 'src',
},
vite: {
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
paraglide({
project: './project.inlang',
outdir: './src/paraglide',
outputStructure: 'message-modules',
cookieName: 'locale',
strategy: ['cookie', 'preferredLanguage', 'baseLocale'],
}),
],
},
server: {
preset: 'bun',
},
});

34
biome.json Normal file
View File

@ -0,0 +1,34 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["./src/routeTree.gen.ts", "./.vscode/**", "./src/**/*.css"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedImports": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"indentWidth": 2
}
}
}

3046
bun.lock Normal file

File diff suppressed because it is too large Load Diff

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/styles/app.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

12
drizzle.config.ts Normal file
View File

@ -0,0 +1,12 @@
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
import env from '@/env';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema/index.ts',
dialect: 'postgresql',
dbCredentials: {
url: env.DATABASE_URL,
},
});

View File

@ -0,0 +1,46 @@
CREATE OR REPLACE FUNCTION enum_array_to_bitmask(anyarray)
RETURNS bigint AS $$
DECLARE
result bigint := 0;
val text;
enum_vals text[];
i int;
BEGIN
-- Get all possible values of the enum type
EXECUTE format('SELECT enum_range(NULL::%s)', pg_typeof($1[1])::text) INTO enum_vals;
-- Loop through input array and compute the bitmask
FOREACH val IN ARRAY $1 LOOP
FOR i IN 1 .. array_length(enum_vals, 1) LOOP
IF enum_vals[i] = val THEN
result := result + (1::bigint << (i - 1));
EXIT;
END IF;
END LOOP;
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
--> statement-breakpoint
CREATE OR REPLACE FUNCTION enum_to_bitmask(anyenum)
RETURNS bigint AS $$
DECLARE
enum_vals text[];
val text := $1::text;
i int;
BEGIN
-- Get all possible enum values of the input type
EXECUTE format('SELECT enum_range(NULL::%s)', pg_typeof($1)::text) INTO enum_vals;
-- Find the position of the value
FOR i IN 1 .. array_length(enum_vals, 1) LOOP
IF enum_vals[i] = val THEN
RETURN i;
END IF;
END LOOP;
-- If not found return 0
RETURN 0;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

913
drizzle/0001_init.sql Normal file
View File

@ -0,0 +1,913 @@
CREATE SCHEMA "account";
--> statement-breakpoint
CREATE SCHEMA "common";
--> statement-breakpoint
CREATE SCHEMA "log";
--> statement-breakpoint
CREATE SCHEMA "player";
--> statement-breakpoint
CREATE TYPE "common"."gm_authority" AS ENUM('IMPLEMENTOR', 'HIGH_WIZARD', 'GOD', 'LOW_WIZARD', 'PLAYER');--> statement-breakpoint
CREATE TYPE "common"."spam_type" AS ENUM('GOOD', 'SPAM');--> statement-breakpoint
CREATE TYPE "log"."gold_log_how" AS ENUM('BUY', 'SELL', 'SHOP_SELL', 'SHOP_BUY', 'EXCHANGE_TAKE', 'EXCHANGE_GIVE', 'QUEST');--> statement-breakpoint
CREATE TYPE "log"."log_type" AS ENUM('ITEM', 'CHARACTER');--> statement-breakpoint
CREATE TYPE "log"."login_log_type" AS ENUM('LOGIN', 'LOGOUT');--> statement-breakpoint
CREATE TYPE "log"."money_log_type" AS ENUM('MONSTER', 'SHOP', 'REFINE', 'QUEST', 'GUILD', 'MISC', 'KILL', 'DROP');--> statement-breakpoint
CREATE TYPE "log"."refine_log_set_type" AS ENUM('SOCKET', 'POWER', 'ROD', 'GUILD', 'SCROLL', 'HYUNIRON', 'GOD_SCROLL', 'MUSIN_SCROLL');--> statement-breakpoint
CREATE TYPE "log"."reward_type" AS ENUM('EXP', 'ITEM');--> statement-breakpoint
CREATE TYPE "player"."guild_grade_auth" AS ENUM('ADD_MEMBER', 'REMOVE_MEMEBER', 'NOTICE', 'USE_SKILL');--> statement-breakpoint
CREATE TYPE "player"."item_proto_immune_flag" AS ENUM('PARA', 'CURSE', 'STUN', 'SLEEP', 'SLOW', 'POISON', 'TERROR');--> statement-breakpoint
CREATE TYPE "player"."item_attr_apply" AS ENUM('MAX_HP', 'MAX_SP', 'CON', 'INT', 'STR', 'DEX', 'ATT_SPEED', 'MOV_SPEED', 'CAST_SPEED', 'HP_REGEN', 'SP_REGEN', 'POISON_PCT', 'STUN_PCT', 'SLOW_PCT', 'CRITICAL_PCT', 'PENETRATE_PCT', 'ATTBONUS_HUMAN', 'ATTBONUS_ANIMAL', 'ATTBONUS_ORC', 'ATTBONUS_MILGYO', 'ATTBONUS_UNDEAD', 'ATTBONUS_DEVIL', 'STEAL_HP', 'STEAL_SP', 'MANA_BURN_PCT', 'DAMAGE_SP_RECOVER', 'BLOCK', 'DODGE', 'RESIST_SWORD', 'RESIST_TWOHAND', 'RESIST_DAGGER', 'RESIST_BELL', 'RESIST_FAN', 'RESIST_BOW', 'RESIST_FIRE', 'RESIST_ELEC', 'RESIST_MAGIC', 'RESIST_WIND', 'REFLECT_MELEE', 'REFLECT_CURSE', 'POISON_REDUCE', 'KILL_SP_RECOVER', 'EXP_DOUBLE_BONUS', 'GOLD_DOUBLE_BONUS', 'ITEM_DROP_BONUS', 'POTION_BONUS', 'KILL_HP_RECOVER', 'IMMUNE_STUN', 'IMMUNE_SLOW', 'IMMUNE_FALL', 'SKILL', 'BOW_DISTANCE', 'ATT_GRADE_BONUS', 'DEF_GRADE_BONUS', 'MAGIC_ATT_GRADE', 'MAGIC_DEF_GRADE', 'CURSE_PCT', 'MAX_STAMINA', 'ATTBONUS_WARRIOR', 'ATTBONUS_ASSASSIN', 'ATTBONUS_SURA', 'ATTBONUS_SHAMAN', 'ATTBONUS_MONSTER', 'MALL_ATTBONUS', 'MALL_DEFBONUS', 'MALL_EXPBONUS', 'MALL_ITEMBONUS', 'MALL_GOLDBONUS', 'MAX_HP_PCT', 'MAX_SP_PCT', 'SKILL_DAMAGE_BONUS', 'NORMAL_HIT_DAMAGE_BONUS', 'SKILL_DEFEND_BONUS', 'NORMAL_HIT_DEFEND_BONUS', 'PC_BANG_EXP_BONUS', 'PC_BANG_DROP_BONUS', 'EXTRACT_HP_PCT', 'RESIST_WARRIOR', 'RESIST_ASSASSIN', 'RESIST_SURA', 'RESIST_SHAMAN', 'ENERGY', 'DEF_GRADE', 'COSTUME_ATTR_BONUS', 'MAGIC_ATTBONUS_PER', 'MELEE_MAGIC_ATTBONUS_PER', 'RESIST_ICE', 'RESIST_EARTH', 'RESIST_DARK', 'ANTI_CRITICAL_PCT', 'ANTI_PENETRATE_PCT');--> statement-breakpoint
CREATE TYPE "player"."item_attr_rare_apply" AS ENUM('MAX_HP', 'MAX_SP', 'CON', 'INT', 'STR', 'DEX', 'ATT_SPEED', 'MOV_SPEED', 'CAST_SPEED', 'HP_REGEN', 'SP_REGEN', 'POISON_PCT', 'STUN_PCT', 'SLOW_PCT', 'CRITICAL_PCT', 'PENETRATE_PCT', 'ATTBONUS_HUMAN', 'ATTBONUS_ANIMAL', 'ATTBONUS_ORC', 'ATTBONUS_MILGYO', 'ATTBONUS_UNDEAD', 'ATTBONUS_DEVIL', 'STEAL_HP', 'STEAL_SP', 'MANA_BURN_PCT', 'DAMAGE_SP_RECOVER', 'BLOCK', 'DODGE', 'RESIST_SWORD', 'RESIST_TWOHAND', 'RESIST_DAGGER', 'RESIST_BELL', 'RESIST_FAN', 'RESIST_BOW', 'RESIST_FIRE', 'RESIST_ELEC', 'RESIST_MAGIC', 'RESIST_WIND', 'REFLECT_MELEE', 'REFLECT_CURSE', 'POISON_REDUCE', 'KILL_SP_RECOVER', 'EXP_DOUBLE_BONUS', 'GOLD_DOUBLE_BONUS', 'ITEM_DROP_BONUS', 'POTION_BONUS', 'KILL_HP_RECOVER', 'IMMUNE_STUN', 'IMMUNE_SLOW', 'IMMUNE_FALL', 'SKILL', 'BOW_DISTANCE', 'ATT_GRADE_BONUS', 'DEF_GRADE_BONUS', 'MAGIC_ATT_GRADE', 'MAGIC_DEF_GRADE', 'CURSE_PCT', 'MAX_STAMINA', 'ATT_BONUS_TO_WARRIOR', 'ATT_BONUS_TO_ASSASSIN', 'ATT_BONUS_TO_SURA', 'ATT_BONUS_TO_SHAMAN', 'ATT_BONUS_TO_MONSTER', 'NORMAL_HIT_DEFEND_BONUS', 'SKILL_DEFEND_BONUS', 'NOUSE2''NOUSE3', 'NOUSE4', 'NOUSE5', 'NOUSE6', 'NOUSE7', 'NOUSE8', 'NOUSE9', 'NOUSE10', 'NOUSE11', 'NOUSE12', 'NOUSE13', 'NOUSE14', 'RESIST_WARRIOR', 'RESIST_ASSASSIN', 'RESIST_SURA', 'RESIST_SHAMAN');--> statement-breakpoint
CREATE TYPE "player"."item_window" AS ENUM('INVENTORY', 'EQUIPMENT', 'SAFEBOX', 'MALL', 'DRAGON_SOUL_INVENTORY', 'BELT_INVENTORY');--> statement-breakpoint
CREATE TYPE "player"."mob_ai_flag" AS ENUM('AGGR', 'NOMOVE', 'COWARD', 'NOATTSHINSU', 'NOATTCHUNJO', 'NOATTJINNO', 'ATTMOB', 'BERSERK', 'STONESKIN', 'GODSPEED', 'DEATHBLOW', 'REVIVE');--> statement-breakpoint
CREATE TYPE "player"."mob_set_immune_flag" AS ENUM('STUN', 'SLOW', 'FALL', 'CURSE', 'POISON', 'TERROR');--> statement-breakpoint
CREATE TYPE "player"."mob_set_race_flag" AS ENUM('ANIMAL', 'UNDEAD', 'DEVIL', 'HUMAN', 'ORC', 'MILGYO', 'INSECT', 'FIRE', 'ICE', 'DESERT', 'TREE', 'ATT_ELEC', 'ATT_FIRE', 'ATT_ICE', 'ATT_WIND', 'ATT_EARTH', 'ATT_DARK');--> statement-breakpoint
CREATE TYPE "player"."mob_size" AS ENUM('SMALL', 'MEDIUM', 'BIG');--> statement-breakpoint
CREATE TYPE "player"."skill_proto_set_affect_flag" AS ENUM('YMIR', 'INVISIBILITY', 'SPAWN', 'POISON', 'SLOW', 'STUN', 'DUNGEON_READY', 'FORCE_VISIBLE', 'BUILDING_CONSTRUCTION_SMALL', 'BUILDING_CONSTRUCTION_LARGE', 'BUILDING_UPGRADE', 'MOV_SPEED_POTION', 'ATT_SPEED_POTION', 'FISH_MIDE', 'JEONGWIHON', 'GEOMGYEONG', 'CHEONGEUN', 'GYEONGGONG', 'EUNHYUNG', 'GWIGUM', 'TERROR', 'JUMAGAP', 'HOSIN', 'BOHO', 'KWAESOK', 'MANASHIELD', 'MUYEONG', 'REVIVE_INVISIBLE', 'FIRE', 'GICHEON', 'JEUNGRYEOK');--> statement-breakpoint
CREATE TYPE "player"."skill_proto_set_affect_flag_2" AS ENUM('YMIR', 'INVISIBILITY', 'SPAWN', 'POISON', 'SLOW', 'STUN', 'DUNGEON_READY', 'FORCE_VISIBLE', 'BUILDING_CONSTRUCTION_SMALL', 'BUILDING_CONSTRUCTION_LARGE', 'BUILDING_UPGRADE', 'MOV_SPEED_POTION', 'ATT_SPEED_POTION', 'FISH_MIDE', 'JEONGWIHON', 'GEOMGYEONG', 'CHEONGEUN', 'GYEONGGONG', 'EUNHYUNG', 'GWIGUM', 'TERROR', 'JUMAGAP', 'HOSIN', 'BOHO', 'KWAESOK', 'MANASHIELD');--> statement-breakpoint
CREATE TYPE "player"."skill_proto_set_flag" AS ENUM('ATTACK', 'USE_MELEE_DAMAGE', 'COMPUTE_ATTGRADE', 'SELFONLY', 'USE_MAGIC_DAMAGE', 'USE_HP_AS_COST', 'COMPUTE_MAGIC_DAMAGE', 'SPLASH', 'GIVE_PENALTY', 'USE_ARROW_DAMAGE', 'PENETRATE', 'IGNORE_TARGET_RATING', 'ATTACK_SLOW', 'ATTACK_STUN', 'HP_ABSORB', 'SP_ABSORB', 'ATTACK_FIRE_CONT', 'REMOVE_BAD_AFFECT', 'REMOVE_GOOD_AFFECT', 'CRUSH', 'ATTACK_POISON', 'TOGGLE', 'DISABLE_BY_POINT_UP', 'CRUSH_LONG');--> statement-breakpoint
CREATE TYPE "player"."skill_proto_skill_type" AS ENUM('NORMAL', 'MELEE', 'RANGE', 'MAGIC');--> statement-breakpoint
CREATE TABLE "account"."account" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "account"."account_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"login" varchar(30) NOT NULL,
"password" text NOT NULL,
"social_id" varchar(13) NOT NULL,
"email" varchar(64) NOT NULL,
"status" varchar(8) DEFAULT 'OK' NOT NULL,
"security_code" varchar(192),
"available_dt" timestamp DEFAULT now() NOT NULL,
"mileage" integer DEFAULT 0 NOT NULL,
"cash" integer DEFAULT 0 NOT NULL,
"gold_expired_at" timestamp DEFAULT now() NOT NULL,
"silver_expired_at" timestamp DEFAULT now() NOT NULL,
"safebox_expired_at" timestamp DEFAULT now() NOT NULL,
"autoloot_expired_at" timestamp DEFAULT now() NOT NULL,
"fish_mind_expired_at" timestamp DEFAULT now() NOT NULL,
"marriage_fast_expired_at" timestamp DEFAULT now() NOT NULL,
"money_drop_rate_expired_at" timestamp DEFAULT now() NOT NULL,
"ip" varchar(191),
"created_at" timestamp DEFAULT now() NOT NULL,
"last_played_at" timestamp,
CONSTRAINT "account_login" UNIQUE("login"),
CONSTRAINT "account_email" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "common"."gm_host" (
"ip" varchar(16) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "common"."gm_list" (
"player_id" integer NOT NULL,
"contact_ip" varchar(16) NOT NULL,
"server_ip" varchar(16) DEFAULT 'ALL' NOT NULL,
"authority" "common"."gm_authority" DEFAULT 'PLAYER'
);
--> statement-breakpoint
CREATE TABLE "common"."locale" (
"key" varchar(191) NOT NULL,
"value" varchar(191) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "common"."spam_db" (
"type" "common"."spam_type" DEFAULT 'SPAM' NOT NULL,
"word" varchar(256) NOT NULL,
"score" integer DEFAULT 10 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "log"."boot_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."boot_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"hostname" char(128) DEFAULT 'UNKNOWN' NOT NULL,
"channel" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."change_name" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."change_name_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"old_name" varchar(191),
"new_name" varchar(191),
"ip" varchar(20),
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."command_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."command_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"server" integer NOT NULL,
"ip" varchar(15) NOT NULL,
"port" integer NOT NULL,
"username" varchar(50) NOT NULL,
"command" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "log"."cube_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."cube_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"item_vnum" integer DEFAULT 0 NOT NULL,
"item_uid" integer DEFAULT 0 NOT NULL,
"item_count" integer DEFAULT 0 NOT NULL,
"success" boolean NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."dragon_slay_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."dragon_slay_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"guild_id" integer NOT NULL,
"vnum" integer NOT NULL,
"start_time" timestamp DEFAULT now() NOT NULL,
"end_time" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "log"."fish_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."fish_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"map_index" integer DEFAULT 0 NOT NULL,
"fish_id" integer DEFAULT 0 NOT NULL,
"fishing_level" integer DEFAULT 0 NOT NULL,
"waiting_time" integer DEFAULT 0 NOT NULL,
"success" boolean NOT NULL,
"size" integer NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."gold_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."gold_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"what" integer DEFAULT 0 NOT NULL,
"how" "log"."gold_log_how" NOT NULL,
"hint" varchar(50),
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."hack_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."hack_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"login" char(16) NOT NULL,
"name" char(24) NOT NULL,
"ip" char(15) NOT NULL,
"server" char(100) NOT NULL,
"why" char(191) NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."level_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."level_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"name" char(24) NOT NULL,
"level" integer DEFAULT 0 NOT NULL,
"playtime" integer DEFAULT 0 NOT NULL,
"player_id" integer NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"type" "log"."log_type" DEFAULT 'ITEM' NOT NULL,
"who" integer DEFAULT 0 NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"what" integer DEFAULT 0 NOT NULL,
"how" varchar(50) NOT NULL,
"hint" varchar(70),
"ip" varchar(20),
"vnum" integer,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."login_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."login_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"type" "log"."login_log_type",
"channel" integer,
"account_id" integer NOT NULL,
"player_id" integer NOT NULL,
"level" integer,
"job" integer,
"playtime" integer,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."login_log_2" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."login_log_2_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"type" text,
"is_gm" boolean,
"channel" integer,
"account_id" integer NOT NULL,
"player_id" integer NOT NULL,
"ip" text,
"client_version" text,
"playtime" integer DEFAULT 0 NOT NULL,
"login_at" timestamp,
"logout_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "log"."money_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."money_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"type" "log"."money_log_type",
"vnum" integer DEFAULT 0 NOT NULL,
"gold" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."player_count" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."player_count_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"count_red" integer,
"count_yellow" integer,
"count_blue" integer,
"count_total" integer,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."quest_reward_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."quest_reward_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"quest_name" varchar(32),
"player_id" integer NOT NULL,
"player_level" integer,
"reward_type" "log"."reward_type",
"reward_value1" integer,
"reward_value2" integer,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."refine_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."refine_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"item_name" varchar(24) NOT NULL,
"item_id" integer DEFAULT 0 NOT NULL,
"step" varchar(50) NOT NULL,
"is_success" boolean NOT NULL,
"set_type" "log"."refine_log_set_type" NOT NULL,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."shout_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."shout_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"channel" integer,
"empire" integer,
"shout" varchar(350),
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "log"."speed_hack_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "log"."speed_hack_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"x" integer,
"y" integer,
"hack_count" integer,
"created_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "player"."affect" (
"player_id" integer NOT NULL,
"type" integer NOT NULL,
"apply_on" integer DEFAULT 0 NOT NULL,
"apply_value" integer DEFAULT 0 NOT NULL,
"flag" integer DEFAULT 0 NOT NULL,
"duration" integer DEFAULT 0 NOT NULL,
"sp_cost" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."banword" (
"word" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."change_empire" (
"account_id" integer NOT NULL,
"change_count" integer,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."guild" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."guild_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"name" varchar(12) NOT NULL,
"sp" integer DEFAULT 1000 NOT NULL,
"master" integer DEFAULT 0 NOT NULL,
"level" integer,
"exp" integer,
"skill_point" integer DEFAULT 0 NOT NULL,
"skill" "bytea",
"win" integer DEFAULT 0 NOT NULL,
"draw" integer DEFAULT 0 NOT NULL,
"loss" integer DEFAULT 0 NOT NULL,
"ladder_point" integer DEFAULT 0 NOT NULL,
"gold" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."guild_comment" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."guild_comment_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"guild_id" integer NOT NULL,
"name" varchar(8),
"notice" integer,
"content" varchar(50),
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."guild_grade" (
"guild_id" integer NOT NULL,
"grade" integer DEFAULT 0 NOT NULL,
"name" varchar(8) NOT NULL,
"auth" "guild_grade_auth"[] DEFAULT '{}' NOT NULL,
"auth_bits" bigint GENERATED ALWAYS AS (enum_array_to_bitmask("player"."guild_grade"."auth")) STORED
);
--> statement-breakpoint
CREATE TABLE "player"."guild_member" (
"player_id" integer NOT NULL,
"guild_id" integer NOT NULL,
"grade" integer,
"is_general" boolean DEFAULT false NOT NULL,
"offer" integer,
CONSTRAINT "guild_member_player_id_guild_id_pk" PRIMARY KEY("player_id","guild_id"),
CONSTRAINT "player_id" UNIQUE("player_id")
);
--> statement-breakpoint
CREATE TABLE "player"."guild_war_bet" (
"login" varchar(24) NOT NULL,
"gold" integer DEFAULT 0 NOT NULL,
"guild_id" integer NOT NULL,
"war_id" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."guild_war_reservation" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."guild_war_reservation_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"guild1" integer NOT NULL,
"guild2" integer NOT NULL,
"type" integer DEFAULT 0 NOT NULL,
"warprice" integer DEFAULT 0 NOT NULL,
"initial_score" integer DEFAULT 0 NOT NULL,
"started" integer DEFAULT 0 NOT NULL,
"bet_from" integer DEFAULT 0 NOT NULL,
"bet_to" integer DEFAULT 0 NOT NULL,
"winner" integer DEFAULT -1 NOT NULL,
"power_from" integer DEFAULT 0 NOT NULL,
"power_to" integer DEFAULT 0 NOT NULL,
"handicap" integer DEFAULT 0 NOT NULL,
"result_1" integer DEFAULT 0 NOT NULL,
"result_2" integer DEFAULT 0 NOT NULL,
"started_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."horse_name" (
"player_id" integer NOT NULL,
"name" varchar(24) DEFAULT 'NONAME' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."item" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."item_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"owner_id" integer NOT NULL,
"window" "player"."item_window" DEFAULT 'INVENTORY' NOT NULL,
"position" integer NOT NULL,
"count" integer DEFAULT 0 NOT NULL,
"vnum" integer DEFAULT 0 NOT NULL,
"socket_0" integer DEFAULT 0 NOT NULL,
"socket_1" integer DEFAULT 0 NOT NULL,
"socket_2" integer DEFAULT 0 NOT NULL,
"socket_3" integer DEFAULT 0 NOT NULL,
"socket_4" integer DEFAULT 0 NOT NULL,
"socket_5" integer DEFAULT 0 NOT NULL,
"attr_type_0" integer DEFAULT 0 NOT NULL,
"attr_value_0" integer DEFAULT 0 NOT NULL,
"attr_type_1" integer DEFAULT 0 NOT NULL,
"attr_value_1" integer DEFAULT 0 NOT NULL,
"attr_type_2" integer DEFAULT 0 NOT NULL,
"attr_value_2" integer DEFAULT 0 NOT NULL,
"attr_type_3" integer DEFAULT 0 NOT NULL,
"attr_value_3" integer DEFAULT 0 NOT NULL,
"attr_type_4" integer DEFAULT 0 NOT NULL,
"attr_value_4" integer DEFAULT 0 NOT NULL,
"attr_type_5" integer DEFAULT 0 NOT NULL,
"attr_value_5" integer DEFAULT 0 NOT NULL,
"attr_type_6" integer DEFAULT 0 NOT NULL,
"attr_value_6" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."item_attr" (
"apply" "player"."item_attr_apply" DEFAULT 'MAX_HP' NOT NULL,
"probability" integer NOT NULL,
"lvl_1" integer NOT NULL,
"lvl_2" integer NOT NULL,
"lvl_3" integer NOT NULL,
"lvl_4" integer NOT NULL,
"lvl_5" integer NOT NULL,
"weapon" integer NOT NULL,
"body" integer NOT NULL,
"wrist" integer NOT NULL,
"foots" integer NOT NULL,
"neck" integer NOT NULL,
"head" integer NOT NULL,
"shield" integer NOT NULL,
"ear" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."item_attr_rare" (
"apply" "player"."item_attr_rare_apply" DEFAULT 'MAX_HP' NOT NULL,
"probability" integer NOT NULL,
"lvl_1" integer NOT NULL,
"lvl_2" integer NOT NULL,
"lvl_3" integer NOT NULL,
"lvl_4" integer NOT NULL,
"lvl_5" integer NOT NULL,
"weapon" integer NOT NULL,
"body" integer NOT NULL,
"wrist" integer NOT NULL,
"foots" integer NOT NULL,
"neck" integer NOT NULL,
"head" integer NOT NULL,
"shield" integer NOT NULL,
"ear" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."item_award" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."item_award_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"player_id" integer NOT NULL,
"login" varchar(30) NOT NULL,
"vnum" integer DEFAULT 0 NOT NULL,
"count" integer DEFAULT 0 NOT NULL,
"give_at" timestamp DEFAULT now() NOT NULL,
"taken_at" timestamp,
"item_id" integer,
"why" varchar(128),
"socket_0" integer DEFAULT 0 NOT NULL,
"socket_1" integer DEFAULT 0 NOT NULL,
"socket_2" integer DEFAULT 0 NOT NULL,
"mall" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."item_proto" (
"vnum" integer DEFAULT 0 NOT NULL,
"name" text NOT NULL,
"locale_name" text NOT NULL,
"type" integer DEFAULT 0 NOT NULL,
"sub_type" integer DEFAULT 0 NOT NULL,
"weight" integer DEFAULT 0,
"size" integer DEFAULT 0,
"anti_flag" integer DEFAULT 0,
"flag" integer DEFAULT 0,
"wear_flag" integer DEFAULT 0,
"immune_flag" "item_proto_immune_flag"[] DEFAULT '{}' NOT NULL,
"gold" integer DEFAULT 0,
"shop_buy_price" integer DEFAULT 0 NOT NULL,
"refined_vnum" integer DEFAULT 0 NOT NULL,
"refine_set" integer NOT NULL,
"refine_set2" integer NOT NULL,
"magic_percentage" integer DEFAULT 0 NOT NULL,
"limit_type_0" integer DEFAULT 0,
"limit_value_0" integer DEFAULT 0,
"limit_type_1" integer DEFAULT 0,
"limit_value_1" integer DEFAULT 0,
"apply_type_0" integer DEFAULT 0,
"apply_value_0" integer DEFAULT 0,
"apply_type_1" integer DEFAULT 0,
"apply_value_1" integer DEFAULT 0,
"apply_type_2" integer DEFAULT 0,
"apply_value_2" integer DEFAULT 0,
"value_0" integer DEFAULT 0,
"value_1" integer DEFAULT 0,
"value_2" integer DEFAULT 0,
"value_3" integer DEFAULT 0,
"value_4" integer DEFAULT 0,
"value_5" integer DEFAULT 0,
"socket_0" integer DEFAULT -1,
"socket_1" integer DEFAULT -1,
"socket_2" integer DEFAULT -1,
"socket_3" integer DEFAULT -1,
"socket_4" integer DEFAULT -1,
"socket_5" integer DEFAULT -1,
"specular" integer DEFAULT 0 NOT NULL,
"socket_percentage" integer DEFAULT 0 NOT NULL,
"addon_type" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."land" (
"id" integer PRIMARY KEY NOT NULL,
"map_index" integer DEFAULT 0 NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"width" integer DEFAULT 0 NOT NULL,
"height" integer DEFAULT 0 NOT NULL,
"guild_id" integer DEFAULT 0 NOT NULL,
"guild_level_limit" integer DEFAULT 0 NOT NULL,
"price" integer DEFAULT 0 NOT NULL,
"enabled" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."lotto_list" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."lotto_list_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"server" varchar(20),
"playerId" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."marriage" (
"is_married" boolean DEFAULT false NOT NULL,
"player_id_1" integer NOT NULL,
"player_id_2" integer NOT NULL,
"love_points" integer,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."messenger_list" (
"account" varchar(16) NOT NULL,
"companion" varchar(16) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."mob_proto" (
"vnum" integer DEFAULT 0 NOT NULL,
"name" text NOT NULL,
"locale_name" text NOT NULL,
"rank" integer DEFAULT 0 NOT NULL,
"type" integer DEFAULT 0 NOT NULL,
"battle_type" integer DEFAULT 0 NOT NULL,
"level" integer DEFAULT 1 NOT NULL,
"size" "player"."mob_size" DEFAULT 'SMALL',
"ai_flag" "mob_ai_flag"[] DEFAULT '{}' NOT NULL,
"mount_capacity" integer DEFAULT 0 NOT NULL,
"set_race_flag" "player"."mob_set_race_flag" NOT NULL,
"set_immune_flag" "player"."mob_set_immune_flag",
"empire" integer DEFAULT 0 NOT NULL,
"folder" varchar(100) NOT NULL,
"on_click" integer DEFAULT 0 NOT NULL,
"st" integer NOT NULL,
"dx" integer NOT NULL,
"ht" integer NOT NULL,
"iq" integer NOT NULL,
"damage_min" integer NOT NULL,
"damage_max" integer NOT NULL,
"max_hp" integer DEFAULT 0 NOT NULL,
"regen_cycle" integer DEFAULT 0 NOT NULL,
"regen_percent" integer DEFAULT 0 NOT NULL,
"gold_min" integer DEFAULT 0 NOT NULL,
"gold_max" integer DEFAULT 0 NOT NULL,
"exp" integer DEFAULT 0 NOT NULL,
"def" integer NOT NULL,
"attack_speed" integer DEFAULT 100 NOT NULL,
"move_speed" integer DEFAULT 100 NOT NULL,
"aggressive_hp_percentage" integer DEFAULT 0 NOT NULL,
"aggressive_sight" integer NOT NULL,
"attack_range" integer NOT NULL,
"drop_item" integer DEFAULT 0 NOT NULL,
"resurrection_vnum" integer DEFAULT 0 NOT NULL,
"enchant_curse" integer DEFAULT 0 NOT NULL,
"enchant_slow" integer DEFAULT 0 NOT NULL,
"enchant_poison" integer DEFAULT 0 NOT NULL,
"enchant_stun" integer DEFAULT 0 NOT NULL,
"enchant_critical" integer DEFAULT 0 NOT NULL,
"enchant_penetrate" integer DEFAULT 0 NOT NULL,
"resist_sword" integer DEFAULT 0 NOT NULL,
"resist_twohand" integer DEFAULT 0 NOT NULL,
"resist_dagger" integer DEFAULT 0 NOT NULL,
"resist_bell" integer DEFAULT 0 NOT NULL,
"resist_fan" integer DEFAULT 0 NOT NULL,
"resist_bow" integer DEFAULT 0 NOT NULL,
"resist_fire" integer DEFAULT 0 NOT NULL,
"resist_elect" integer DEFAULT 0 NOT NULL,
"resist_magic" integer DEFAULT 0 NOT NULL,
"resist_wind" integer DEFAULT 0 NOT NULL,
"resist_poison" integer DEFAULT 0 NOT NULL,
"dam_multiply" double precision,
"summon" integer,
"drain_sp" integer,
"mob_color" integer,
"polymorph_item" integer DEFAULT 0 NOT NULL,
"skill_level0" integer,
"skill_vnum0" integer,
"skill_level1" integer,
"skill_vnum1" integer,
"skill_level2" integer,
"skill_vnum2" integer,
"skill_level3" integer,
"skill_vnum3" integer,
"skill_level4" integer,
"skill_vnum4" integer,
"sp_berserk" integer DEFAULT 0 NOT NULL,
"sp_stoneskin" integer DEFAULT 0 NOT NULL,
"sp_godspeed" integer DEFAULT 0 NOT NULL,
"sp_deathblow" integer DEFAULT 0 NOT NULL,
"sp_revive" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."monarch" (
"empire" integer DEFAULT 0 NOT NULL,
"player_id" integer NOT NULL,
"win_at" timestamp,
"money" integer
);
--> statement-breakpoint
CREATE TABLE "player"."monarch_candidacy" (
"player_id" integer NOT NULL,
"date" timestamp DEFAULT now(),
"name" varchar(16),
"win_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "player"."monarch_election" (
"player_id" integer NOT NULL,
"selected_player_id" integer NOT NULL,
"election_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE "player"."myshop_pricelist" (
"owner_id" integer NOT NULL,
"item_vnum" integer DEFAULT 0 NOT NULL,
"price" integer DEFAULT 0 NOT NULL,
CONSTRAINT "list_id" UNIQUE("owner_id","item_vnum")
);
--> statement-breakpoint
CREATE TABLE "player"."object" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."object_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"land_id" integer DEFAULT 0 NOT NULL,
"vnum" integer DEFAULT 0 NOT NULL,
"map_index" integer DEFAULT 0 NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"x_rot" double precision NOT NULL,
"y_rot" double precision NOT NULL,
"z_rot" double precision NOT NULL,
"life" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."object_proto" (
"vnum" integer DEFAULT 0 NOT NULL,
"name" varchar(32) NOT NULL,
"price" integer DEFAULT 0 NOT NULL,
"materials" varchar(64) NOT NULL,
"upgrade_vnum" integer DEFAULT 0 NOT NULL,
"upgrade_limit_time" integer DEFAULT 0 NOT NULL,
"life" integer DEFAULT 0 NOT NULL,
"reg_1" integer DEFAULT 0 NOT NULL,
"reg_2" integer DEFAULT 0 NOT NULL,
"reg_3" integer DEFAULT 0 NOT NULL,
"reg_4" integer DEFAULT 0 NOT NULL,
"npc" integer DEFAULT 0 NOT NULL,
"group_vnum" integer DEFAULT 0 NOT NULL,
"dependent_group" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."player" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."player_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"account_id" integer NOT NULL,
"name" varchar(24) DEFAULT 'NONAME' NOT NULL,
"job" integer DEFAULT 0 NOT NULL,
"voice" integer DEFAULT 0 NOT NULL,
"dir" integer DEFAULT 0 NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"z" integer DEFAULT 0 NOT NULL,
"map_index" integer DEFAULT 0 NOT NULL,
"exit_x" integer DEFAULT 0 NOT NULL,
"exit_y" integer DEFAULT 0 NOT NULL,
"exit_map_index" integer DEFAULT 0 NOT NULL,
"hp" integer NOT NULL,
"mp" integer NOT NULL,
"stamina" integer NOT NULL,
"random_hp" integer NOT NULL,
"random_sp" integer NOT NULL,
"playtime" integer DEFAULT 0 NOT NULL,
"level" integer DEFAULT 1 NOT NULL,
"level_step" integer DEFAULT 0 NOT NULL,
"st" integer NOT NULL,
"ht" integer NOT NULL,
"dx" integer NOT NULL,
"iq" integer NOT NULL,
"exp" integer DEFAULT 0 NOT NULL,
"gold" integer DEFAULT 0 NOT NULL,
"stat_point" integer NOT NULL,
"skill_point" integer NOT NULL,
"quick_slot" "bytea",
"ip" varchar(15) DEFAULT '0.0.0.0',
"part_main" integer DEFAULT 0 NOT NULL,
"part_base" integer DEFAULT 0 NOT NULL,
"part_hair" integer DEFAULT 0 NOT NULL,
"skill_group" integer DEFAULT 0 NOT NULL,
"skill_level" integer,
"alignment" integer DEFAULT 0 NOT NULL,
"last_played_at" timestamp DEFAULT now() NOT NULL,
"change_name" integer DEFAULT 0 NOT NULL,
"sub_skill_point" integer NOT NULL,
"stat_reset_count" integer DEFAULT 0 NOT NULL,
"horse_hp" integer NOT NULL,
"horse_stamina" integer NOT NULL,
"horse_level" integer DEFAULT 0 NOT NULL,
"horse_hp_droptime" integer DEFAULT 0 NOT NULL,
"horse_riding" integer DEFAULT 0 NOT NULL,
"horse_skill_point" integer NOT NULL,
"bank_value" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."player_deleted" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "player"."player_deleted_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"account_id" integer NOT NULL,
"name" varchar(24) DEFAULT 'NONAME' NOT NULL,
"job" integer DEFAULT 0 NOT NULL,
"voice" integer DEFAULT 0 NOT NULL,
"dir" integer DEFAULT 0 NOT NULL,
"x" integer DEFAULT 0 NOT NULL,
"y" integer DEFAULT 0 NOT NULL,
"z" integer DEFAULT 0 NOT NULL,
"map_index" integer DEFAULT 0 NOT NULL,
"exit_x" integer DEFAULT 0 NOT NULL,
"exit_y" integer DEFAULT 0 NOT NULL,
"exit_map_index" integer DEFAULT 0 NOT NULL,
"hp" integer NOT NULL,
"mp" integer NOT NULL,
"stamina" integer NOT NULL,
"random_hp" integer NOT NULL,
"random_sp" integer NOT NULL,
"playtime" integer DEFAULT 0 NOT NULL,
"level" integer DEFAULT 1 NOT NULL,
"level_step" integer DEFAULT 0 NOT NULL,
"st" integer NOT NULL,
"ht" integer NOT NULL,
"dx" integer NOT NULL,
"iq" integer NOT NULL,
"exp" integer DEFAULT 0 NOT NULL,
"gold" integer DEFAULT 0 NOT NULL,
"stat_point" integer NOT NULL,
"skill_point" integer NOT NULL,
"quick_slot" integer,
"ip" varchar(15) DEFAULT '0.0.0.0',
"part_main" integer NOT NULL,
"part_base" integer DEFAULT 0 NOT NULL,
"part_hair" integer NOT NULL,
"skill_group" integer DEFAULT 0 NOT NULL,
"skill_level" integer,
"alignment" integer DEFAULT 0 NOT NULL,
"last_played_at" timestamp DEFAULT now() NOT NULL,
"change_name" integer DEFAULT 0 NOT NULL,
"sub_skill_point" integer NOT NULL,
"stat_reset_count" integer DEFAULT 0 NOT NULL,
"horse_hp" integer NOT NULL,
"horse_stamina" integer NOT NULL,
"horse_level" integer DEFAULT 0 NOT NULL,
"horse_hp_droptime" integer DEFAULT 0 NOT NULL,
"horse_riding" integer DEFAULT 0 NOT NULL,
"horse_skill_point" integer NOT NULL,
"bank_value" integer DEFAULT 0
);
--> statement-breakpoint
CREATE TABLE "player"."player_index" (
"account_id" integer NOT NULL,
"player_id" integer NOT NULL,
"empire" integer DEFAULT 0 NOT NULL,
"last_played_at" timestamp,
CONSTRAINT "player_index_account_id_player_id_pk" PRIMARY KEY("account_id","player_id")
);
--> statement-breakpoint
CREATE TABLE "player"."quest" (
"player_id" integer NOT NULL,
"name" varchar(32) NOT NULL,
"state" varchar(64) NOT NULL,
"value" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."refine_proto" (
"id" integer PRIMARY KEY NOT NULL,
"vnum_0" integer DEFAULT 0 NOT NULL,
"count_0" integer NOT NULL,
"vnum_1" integer DEFAULT 0 NOT NULL,
"count_1" integer NOT NULL,
"vnum_2" integer DEFAULT 0 NOT NULL,
"count_2" integer NOT NULL,
"vnum_3" integer DEFAULT 0 NOT NULL,
"count_3" integer NOT NULL,
"vnum_4" integer DEFAULT 0 NOT NULL,
"count_4" integer NOT NULL,
"cost" integer DEFAULT 0 NOT NULL,
"src_vnum" integer DEFAULT 0 NOT NULL,
"result_vnum" integer DEFAULT 0 NOT NULL,
"probability" integer DEFAULT 100 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."safebox" (
"account_id" integer NOT NULL,
"size" integer DEFAULT 0 NOT NULL,
"password" varchar(6) NOT NULL,
"gold" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."shop" (
"vnum" integer PRIMARY KEY NOT NULL,
"name" varchar(32) DEFAULT 'NONAME' NOT NULL,
"npc_vnum" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player"."shop_item" (
"shop_vnum" integer NOT NULL,
"item_vnum" integer NOT NULL,
"count" integer DEFAULT 1 NOT NULL,
CONSTRAINT "shop_vnum_unique" UNIQUE("shop_vnum","item_vnum","count")
);
--> statement-breakpoint
CREATE TABLE "player"."skill_proto" (
"vnum" integer DEFAULT 0 NOT NULL,
"name" text NOT NULL,
"type" integer DEFAULT 0 NOT NULL,
"level_step" integer DEFAULT 0 NOT NULL,
"max_level" integer DEFAULT 0 NOT NULL,
"level_limit" integer DEFAULT 0 NOT NULL,
"point_on" varchar(100) DEFAULT '0' NOT NULL,
"point_poly" varchar(100) NOT NULL,
"sp_cost_poly" varchar(100) NOT NULL,
"duration_poly" varchar(100) NOT NULL,
"duration_sp_cost_poly" varchar(100) NOT NULL,
"cooldown_poly" varchar(100) NOT NULL,
"master_bonus_poly" varchar(100) NOT NULL,
"attack_grade_poly" varchar(100) NOT NULL,
"set_flag" "skill_proto_set_flag"[] DEFAULT '{}' NOT NULL,
"set_flag_bits" bigint GENERATED ALWAYS AS (enum_array_to_bitmask("player"."skill_proto"."set_flag")) STORED,
"set_affect_flag" "player"."skill_proto_set_affect_flag",
"set_affect_flag_bits" bigint GENERATED ALWAYS AS (enum_to_bitmask("player"."skill_proto"."set_affect_flag")) STORED,
"point_on_2" varchar(100) DEFAULT 'NONE' NOT NULL,
"point_poly_2" varchar(100) NOT NULL,
"duration_poly_2" varchar(100) NOT NULL,
"set_affect_flag_2" "player"."skill_proto_set_affect_flag_2",
"set_affect_flag_2_bits" bigint GENERATED ALWAYS AS (enum_to_bitmask("player"."skill_proto"."set_affect_flag_2")) STORED,
"point_on_3" varchar(100) DEFAULT 'NONE' NOT NULL,
"point_poly_3" varchar(100) NOT NULL,
"duration_poly_3" varchar(100) NOT NULL,
"grand_master_add_sp_cost_poly" varchar(100) NOT NULL,
"prerequisite_skill_vnum" integer DEFAULT 0 NOT NULL,
"prerequisite_skill_level" integer DEFAULT 0 NOT NULL,
"skill_type" "player"."skill_proto_skill_type" DEFAULT 'NORMAL' NOT NULL,
"skill_type_bits" bigint GENERATED ALWAYS AS (enum_to_bitmask("player"."skill_proto"."skill_type")) STORED,
"max_hit" integer DEFAULT 0 NOT NULL,
"splash_around_damage_adjust_poly" varchar(100) DEFAULT '1' NOT NULL,
"target_range" integer DEFAULT 1000 NOT NULL,
"splash_range" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
ALTER TABLE "common"."gm_list" ADD CONSTRAINT "gm_list_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."change_name" ADD CONSTRAINT "change_name_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."command_log" ADD CONSTRAINT "command_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."fish_log" ADD CONSTRAINT "fish_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."gold_log" ADD CONSTRAINT "gold_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."level_log" ADD CONSTRAINT "level_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."login_log" ADD CONSTRAINT "login_log_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."login_log" ADD CONSTRAINT "login_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."login_log_2" ADD CONSTRAINT "login_log_2_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."login_log_2" ADD CONSTRAINT "login_log_2_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."refine_log" ADD CONSTRAINT "refine_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "log"."speed_hack_log" ADD CONSTRAINT "speed_hack_log_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."affect" ADD CONSTRAINT "affect_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."change_empire" ADD CONSTRAINT "change_empire_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_comment" ADD CONSTRAINT "guild_comment_guild_id_guild_id_fk" FOREIGN KEY ("guild_id") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_grade" ADD CONSTRAINT "guild_grade_guild_id_guild_id_fk" FOREIGN KEY ("guild_id") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_member" ADD CONSTRAINT "guild_member_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_member" ADD CONSTRAINT "guild_member_guild_id_guild_id_fk" FOREIGN KEY ("guild_id") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_war_bet" ADD CONSTRAINT "guild_war_bet_guild_id_guild_id_fk" FOREIGN KEY ("guild_id") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_war_bet" ADD CONSTRAINT "guild_war_bet_war_id_guild_war_reservation_id_fk" FOREIGN KEY ("war_id") REFERENCES "player"."guild_war_reservation"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_war_reservation" ADD CONSTRAINT "guild_war_reservation_guild1_guild_id_fk" FOREIGN KEY ("guild1") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."guild_war_reservation" ADD CONSTRAINT "guild_war_reservation_guild2_guild_id_fk" FOREIGN KEY ("guild2") REFERENCES "player"."guild"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."horse_name" ADD CONSTRAINT "horse_name_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."item_award" ADD CONSTRAINT "item_award_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."lotto_list" ADD CONSTRAINT "lotto_list_playerId_player_id_fk" FOREIGN KEY ("playerId") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."marriage" ADD CONSTRAINT "marriage_player_id_1_player_id_fk" FOREIGN KEY ("player_id_1") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."marriage" ADD CONSTRAINT "marriage_player_id_2_player_id_fk" FOREIGN KEY ("player_id_2") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."monarch" ADD CONSTRAINT "monarch_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."monarch_candidacy" ADD CONSTRAINT "monarch_candidacy_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."monarch_election" ADD CONSTRAINT "monarch_election_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."monarch_election" ADD CONSTRAINT "monarch_election_selected_player_id_player_id_fk" FOREIGN KEY ("selected_player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."player" ADD CONSTRAINT "player_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."player_deleted" ADD CONSTRAINT "player_deleted_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."player_index" ADD CONSTRAINT "player_index_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."quest" ADD CONSTRAINT "quest_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "player"."player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."safebox" ADD CONSTRAINT "safebox_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."shop_item" ADD CONSTRAINT "shop_item_shop_vnum_shop_vnum_fk" FOREIGN KEY ("shop_vnum") REFERENCES "player"."shop"("vnum") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_social_id" ON "account"."account" USING btree ("social_id");--> statement-breakpoint
CREATE INDEX "locale_key" ON "common"."locale" USING btree ("key");--> statement-breakpoint
CREATE INDEX "cube_log_player_id" ON "log"."cube_log" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "cube_log_item_vnum" ON "log"."cube_log" USING btree ("item_vnum");--> statement-breakpoint
CREATE INDEX "cube_log_item_uid" ON "log"."cube_log" USING btree ("item_uid");--> statement-breakpoint
CREATE INDEX "gold_log_created_at" ON "log"."gold_log" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "gold_log_player_id" ON "log"."gold_log" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "gold_log_what" ON "log"."gold_log" USING btree ("what");--> statement-breakpoint
CREATE INDEX "gold_log_how" ON "log"."gold_log" USING btree ("how");--> statement-breakpoint
CREATE INDEX "log_who" ON "log"."log" USING btree ("who");--> statement-breakpoint
CREATE INDEX "log_what" ON "log"."log" USING btree ("what");--> statement-breakpoint
CREATE INDEX "log_how" ON "log"."log" USING btree ("how");--> statement-breakpoint
CREATE INDEX "login_log_player_id" ON "log"."login_log" USING btree ("player_id","type");--> statement-breakpoint
CREATE INDEX "money_log_type" ON "log"."money_log" USING btree ("type","vnum");--> statement-breakpoint
CREATE INDEX "quest_reward_log_player_id" ON "log"."quest_reward_log" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "shout_log_created_at" ON "log"."shout_log" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "aaa" ON "player"."guild_comment" USING btree ("notice","id","guild_id");--> statement-breakpoint
CREATE INDEX "guild_comment_guild_id" ON "player"."guild_comment" USING btree ("guild_id");--> statement-breakpoint
CREATE INDEX "item_owner_id" ON "player"."item" USING btree ("owner_id");--> statement-breakpoint
CREATE INDEX "item_window" ON "player"."item" USING btree ("window");--> statement-breakpoint
CREATE INDEX "item_vnum" ON "player"."item" USING btree ("vnum");--> statement-breakpoint
CREATE INDEX "item_award_player_id" ON "player"."item_award" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "item_award_given_at" ON "player"."item_award" USING btree ("give_at");--> statement-breakpoint
CREATE INDEX "item_award_taken_at" ON "player"."item_award" USING btree ("taken_at");--> statement-breakpoint
CREATE INDEX "object_proto_vnum" ON "player"."object_proto" USING btree ("vnum");--> statement-breakpoint
CREATE INDEX "object_proto_group_vnum" ON "player"."object_proto" USING btree ("group_vnum");--> statement-breakpoint
CREATE INDEX "object_proto_upgrade_vnum" ON "player"."object_proto" USING btree ("upgrade_vnum");--> statement-breakpoint
CREATE INDEX "player_account_id" ON "player"."player" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "player_name" ON "player"."player" USING btree ("name");--> statement-breakpoint
CREATE INDEX "player_deleted_account_id" ON "player"."player_deleted" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "player_deleted_name" ON "player"."player_deleted" USING btree ("name");--> statement-breakpoint
CREATE INDEX "player_index_empire" ON "player"."player_index" USING btree ("empire");--> statement-breakpoint
CREATE INDEX "quest_player_id" ON "player"."quest" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "quest_name" ON "player"."quest" USING btree ("name");--> statement-breakpoint
CREATE INDEX "quest_state" ON "player"."quest" USING btree ("state");--> statement-breakpoint
CREATE INDEX "refine_proto_src_vnum" ON "player"."refine_proto" USING btree ("src_vnum");--> statement-breakpoint
CREATE INDEX "refine_proto_result_vnum" ON "player"."refine_proto" USING btree ("result_vnum");--> statement-breakpoint
CREATE INDEX "shop_vnum" ON "player"."shop" USING btree ("vnum");--> statement-breakpoint
CREATE INDEX "shop_item_item_vnum" ON "player"."shop_item" USING btree ("item_vnum");

19
drizzle/0002_seed.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
CREATE SCHEMA "web";
--> statement-breakpoint
CREATE TABLE "web"."password_reset_session" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"email" text NOT NULL,
"code" text NOT NULL,
"expires_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "web"."session" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"ip" varchar(16) DEFAULT '0.0.0.0' NOT NULL,
"expires_at" timestamp NOT NULL
);
--> statement-breakpoint
ALTER TABLE "account"."account" ALTER COLUMN "email" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "account"."account" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "log"."change_name" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "log"."command_log" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "log"."log" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "player"."player" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "player"."player_deleted" ALTER COLUMN "ip" SET DATA TYPE varchar(16);--> statement-breakpoint
ALTER TABLE "web"."password_reset_session" ADD CONSTRAINT "password_reset_session_user_id_account_id_fk" FOREIGN KEY ("user_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "web"."session" ADD CONSTRAINT "session_user_id_account_id_fk" FOREIGN KEY ("user_id") REFERENCES "account"."account"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player"."item_attr_rare" ALTER COLUMN "apply" SET DATA TYPE text;--> statement-breakpoint
DROP TYPE "player"."item_attr_rare_apply";--> statement-breakpoint
CREATE TYPE "player"."item_attr_rare_apply" AS ENUM('MAX_HP', 'MAX_SP', 'CON', 'INT', 'STR', 'DEX', 'ATT_SPEED', 'MOV_SPEED', 'CAST_SPEED', 'HP_REGEN', 'SP_REGEN', 'POISON_PCT', 'STUN_PCT', 'SLOW_PCT', 'CRITICAL_PCT', 'PENETRATE_PCT', 'ATTBONUS_HUMAN', 'ATTBONUS_ANIMAL', 'ATTBONUS_ORC', 'ATTBONUS_MILGYO', 'ATTBONUS_UNDEAD', 'ATTBONUS_DEVIL', 'STEAL_HP', 'STEAL_SP', 'MANA_BURN_PCT', 'DAMAGE_SP_RECOVER', 'BLOCK', 'DODGE', 'RESIST_SWORD', 'RESIST_TWOHAND', 'RESIST_DAGGER', 'RESIST_BELL', 'RESIST_FAN', 'RESIST_BOW', 'RESIST_FIRE', 'RESIST_ELEC', 'RESIST_MAGIC', 'RESIST_WIND', 'REFLECT_MELEE', 'REFLECT_CURSE', 'POISON_REDUCE', 'KILL_SP_RECOVER', 'EXP_DOUBLE_BONUS', 'GOLD_DOUBLE_BONUS', 'ITEM_DROP_BONUS', 'POTION_BONUS', 'KILL_HP_RECOVER', 'IMMUNE_STUN', 'IMMUNE_SLOW', 'IMMUNE_FALL', 'SKILL', 'BOW_DISTANCE', 'ATT_GRADE_BONUS', 'DEF_GRADE_BONUS', 'MAGIC_ATT_GRADE', 'MAGIC_DEF_GRADE', 'CURSE_PCT', 'MAX_STAMINA', 'ATT_BONUS_TO_WARRIOR', 'ATT_BONUS_TO_ASSASSIN', 'ATT_BONUS_TO_SURA', 'ATT_BONUS_TO_SHAMAN', 'ATT_BONUS_TO_MONSTER', 'NORMAL_HIT_DEFEND_BONUS', 'SKILL_DEFEND_BONUS', 'NOUSE2', 'NOUSE3', 'NOUSE4', 'NOUSE5', 'NOUSE6', 'NOUSE7', 'NOUSE8', 'NOUSE9', 'NOUSE10', 'NOUSE11', 'NOUSE12', 'NOUSE13', 'NOUSE14', 'RESIST_WARRIOR', 'RESIST_ASSASSIN', 'RESIST_SURA', 'RESIST_SHAMAN');--> statement-breakpoint
ALTER TABLE "player"."item_attr_rare" ALTER COLUMN "apply" SET DATA TYPE "player"."item_attr_rare_apply" USING "apply"::"player"."item_attr_rare_apply";

View File

@ -0,0 +1,2 @@
ALTER TABLE "web"."password_reset_session" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "web"."session" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;

View File

@ -0,0 +1,18 @@
{
"id": "7ae5f261-ddfc-44f3-a43d-41fb2f5e8eaa",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {},
"enums": {},
"schemas": {},
"views": {},
"sequences": {},
"roles": {},
"policies": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1748477401866,
"tag": "0000_functions",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1748556898510,
"tag": "0001_init",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1748557058507,
"tag": "0002_seed",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1748942645302,
"tag": "0003_worried_hex",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1748943047244,
"tag": "0004_mysterious_wallop",
"breakpoints": true
}
]
}

104
package.json Normal file
View File

@ -0,0 +1,104 @@
{
"name": "metin2-web",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && vinxi build",
"start": "vinxi start",
"lint": "biome lint",
"email:dev": "email dev --dir ./src/emails --port 3030",
"db:generate": "drizzle-kit generate",
"i18n:translate": "inlang machine translate --project ./project.inlang",
"i18n:check": "inlang validate --project ./project.inlang"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-email/components": "0.0.41",
"@tailwindcss/postcss": "^4.1.8",
"@tanstack/react-query": "^5.80.3",
"@tanstack/react-query-devtools": "^5.80.3",
"@tanstack/react-router": "^1.120.15",
"@tanstack/react-router-devtools": "^1.120.15",
"@tanstack/react-start": "^1.120.15",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^1.0.0-beta.1-fd5d1e8",
"drizzle-zod": "^0.8.2",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.511.0",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.3",
"postgres": "^3.4.7",
"react": "^19.1.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.1.0",
"react-hook-form": "^7.57.0",
"react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.0",
"vaul": "^1.1.2",
"vinxi": "0.5.3",
"zod": "^3.25.51"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@inlang/cli": "^3.0.11",
"@inlang/paraglide-js": "^2.0.13",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@types/bun": "^1.2.15",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"autoprefixer": "^10.4.21",
"drizzle-kit": "^1.0.0-beta.1-fd5d1e8",
"postcss": "^8.5.4",
"react-email": "4.0.15",
"tailwindcss": "^4.1.8",
"tw-animate-css": "^1.3.4",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4"
},
"trustedDependencies": [
"@biomejs/biome",
"@parcel/watcher",
"@tailwindcss/oxide",
"argon2",
"esbuild",
"sharp"
]
}

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}

1
project.inlang/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
O51G4WNvcv5kcok0DT

View File

@ -0,0 +1,15 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": [
"en",
"pt"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./src/translations/{locale}.json"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

19
public/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
public/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

9
src/api.ts Normal file
View File

@ -0,0 +1,9 @@
import {
createStartAPIHandler,
defaultAPIFileRouteHandler,
} from '@tanstack/react-start/api';
import { paraglideMiddleware } from './paraglide/server';
export default createStartAPIHandler((ctx) =>
paraglideMiddleware(ctx.request, () => defaultAPIFileRouteHandler(ctx)),
);

14
src/client.tsx Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client';
import { StartClient } from '@tanstack/react-start';
import { createRouter } from './router';
import { getLocale, overwriteGetLocale, strategy } from './paraglide/runtime';
const router = createRouter();
if (strategy.includes('cookie')) {
const inMemoryLocale = getLocale();
overwriteGetLocale(() => inMemoryLocale);
}
hydrateRoot(document, <StartClient router={router} />);

View File

@ -0,0 +1,54 @@
import {
ErrorComponent,
Link,
rootRouteId,
useMatch,
useRouter,
} from '@tanstack/react-router';
import type { ErrorComponentProps } from '@tanstack/react-router';
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
const router = useRouter();
const isRoot = useMatch({
strict: false,
select: (state) => state.id === rootRouteId,
});
console.error('DefaultCatchBoundary Error:', error);
return (
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
<ErrorComponent error={error} />
<div className="flex gap-2 items-center flex-wrap">
<button
type="button"
onClick={() => {
router.invalidate();
}}
className="px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold"
>
Try Again
</button>
{isRoot ? (
<Link
to="/"
className="px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold"
>
Home
</Link>
) : (
<Link
to="/"
className="px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold"
onClick={(e) => {
e.preventDefault();
window.history.back();
}}
>
Go Back
</Link>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,106 @@
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { login, LoginSchema } from '@/lib/actions/auth';
import { isValidationError } from '@/lib/utils/http';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { useMutation } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { useServerFn } from '@tanstack/react-start';
import { LoaderCircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import type { z } from 'zod/v4';
export const LoginForm = () => {
const serverFn = useServerFn(login);
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: standardSchemaResolver(LoginSchema),
});
const mutation = useMutation({
mutationFn: async (payload: z.infer<typeof LoginSchema>) =>
await serverFn({ data: payload }),
onMutate: () => {
return {
toastId: toast.loading('Signing in...'),
};
},
onSuccess: (_data, _variables, context) => {
toast.success('Signed in successfully, redirecting now', {
id: context.toastId,
});
},
onError: (err, _variables, context) => {
toast.error(err.message, {
id: context?.toastId,
});
if (isValidationError<z.infer<typeof LoginSchema>>(err)) {
for (const [key, errors] of Object.entries(err.errors) as [
keyof typeof err.errors,
string[],
][]) {
form.setError(key, { message: errors[0] });
}
}
},
});
const onSubmit = form.handleSubmit((payload) => mutation.mutate(payload));
return (
<Form {...form}>
<form onSubmit={onSubmit} className="grid gap-3">
<FormField
control={form.control}
name="login"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Username" {...field} autoComplete="login" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="Password" type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-0.5">
<Link to="/forgot-password" className="text-sm hover:underline">
Forgot your password?
</Link>
<Link to="/register" className="text-sm hover:underline">
Don&apos;t have an account yet?
</Link>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending && (
<LoaderCircleIcon className="size-4 animate-spin" />
)}
Login
</Button>
</form>
</Form>
);
};

View File

@ -0,0 +1,27 @@
import { Link } from '@tanstack/react-router';
import type { ReactNode } from 'react';
export function NotFound({ children }: { children?: ReactNode }) {
return (
<div className="space-y-2 p-2">
<div className="text-gray-600 dark:text-gray-400">
{children || <p>The page you are looking for does not exist.</p>}
</div>
<p className="flex items-center gap-2 flex-wrap">
<button
type="button"
onClick={() => window.history.back()}
className="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
>
Go back
</button>
<Link
to="/"
className="bg-cyan-600 text-white px-2 py-1 rounded uppercase font-black text-sm"
>
Start Over
</Link>
</p>
</div>
);
}

View File

@ -0,0 +1,64 @@
import type * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,157 @@
"use client"
import type * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,66 @@
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,9 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio }

View File

@ -0,0 +1,53 @@
"use client"
import type * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,46 @@
import type * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,109 @@
import type * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,59 @@
import type * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,73 @@
import type * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
)
}
export { Calendar }

View File

@ -0,0 +1,92 @@
import type * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,241 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

351
src/components/ui/chart.tsx Normal file
View File

@ -0,0 +1,351 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -0,0 +1,32 @@
"use client"
import type * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,177 @@
"use client"
import type * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,252 @@
"use client"
import type * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -0,0 +1,133 @@
import type * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,130 @@
import type * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,257 @@
"use client"
import type * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

165
src/components/ui/form.tsx Normal file
View File

@ -0,0 +1,165 @@
import * as React from "react"
import type * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,42 @@
import type * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -0,0 +1,77 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
)
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
)
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
)
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View File

@ -0,0 +1,21 @@
import type * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -0,0 +1,24 @@
"use client"
import type * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,274 @@
import type * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}

View File

@ -0,0 +1,168 @@
import type * as React from 'react';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority';
import { ChevronDownIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
'group flex flex-1 list-none items-center justify-center gap-1',
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn('relative', className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1',
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
'absolute top-full left-0 isolate z-50 flex justify-center',
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
className,
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

@ -0,0 +1,127 @@
import type * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { type Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@ -0,0 +1,48 @@
"use client"
import type * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,29 @@
import type * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@ -0,0 +1,45 @@
"use client"
import type * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,54 @@
import type * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,58 @@
"use client"
import type * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,183 @@
import type * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -0,0 +1,28 @@
"use client"
import type * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

137
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,137 @@
import type * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,726 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,63 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }

View File

@ -0,0 +1,23 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,31 @@
"use client"
import type * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

114
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,114 @@
import type * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,66 @@
"use client"
import type * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,18 @@
import type * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -0,0 +1,73 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }

View File

@ -0,0 +1,45 @@
import type * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View File

@ -0,0 +1,59 @@
import type * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

6
src/db/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { drizzle } from 'drizzle-orm/bun-sql';
import * as schema from './schema';
import relations from './relations';
import env from '@/env';
export const database = drizzle(env.DATABASE_URL, { schema, relations });

38
src/db/relations.ts Normal file
View File

@ -0,0 +1,38 @@
import { defineRelations } from 'drizzle-orm';
import * as schema from './schema';
const relations = defineRelations(schema, (t) => ({
account: {
players: t.many.player({
from: t.account.id.through(t.playerIndex.playerId),
to: t.player.id.through(t.playerIndex.accountId),
alias: 'account_players',
}),
sessions: t.many.session({
from: t.account.id,
to: t.session.userId,
alias: 'account_sessions'
}),
passwordResetSessions: t.many.passwordResetSession({
from: t.account.id,
to: t.passwordResetSession.userId,
alias: 'account_password_reset_sessions'
})
},
session: {
user: t.one.account({
from: t.session.userId,
to: t.account.id,
alias: 'session_account'
})
},
passwordResetSession: {
user: t.one.account({
from: t.passwordResetSession.userId,
to: t.account.id,
alias: 'password_reset_session_account'
})
}
}));
export default relations;

46
src/db/schema/account.ts Normal file
View File

@ -0,0 +1,46 @@
import {
index,
unique,
integer,
varchar,
text,
timestamp,
pgSchema,
} from 'drizzle-orm/pg-core';
export const accountSchema = pgSchema('account');
export const account = accountSchema.table(
'account',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
login: varchar({ length: 30 }).notNull(),
password: text().notNull(),
socialId: varchar('social_id', { length: 13 }).notNull(),
email: text().notNull(),
status: varchar({ length: 8 }).default('OK').notNull(),
securityCode: varchar('security_code', { length: 192 }),
availDt: timestamp('available_dt').defaultNow().notNull(),
mileage: integer().default(0).notNull(),
cash: integer().default(0).notNull(),
goldExpiredAt: timestamp('gold_expired_at').defaultNow().notNull(),
silverExpiredAt: timestamp('silver_expired_at').defaultNow().notNull(),
safeboxExpiredAt: timestamp('safebox_expired_at').defaultNow().notNull(),
autolootExpiredAt: timestamp('autoloot_expired_at').defaultNow().notNull(),
fishMindExpiredAt: timestamp('fish_mind_expired_at').defaultNow().notNull(),
marriageFastExpiredAt: timestamp('marriage_fast_expired_at')
.defaultNow()
.notNull(),
moneyDropRateExpiredAt: timestamp('money_drop_rate_expired_at')
.defaultNow()
.notNull(),
ip: varchar({ length: 16 }),
createdAt: timestamp('created_at').defaultNow().notNull(),
lastPlayedAt: timestamp('last_played_at'),
},
(table) => [
index('account_social_id').on(table.socialId),
unique('account_login').on(table.login),
unique('account_email').on(table.email),
],
);

42
src/db/schema/common.ts Normal file
View File

@ -0,0 +1,42 @@
import { varchar, integer, pgSchema, index } from 'drizzle-orm/pg-core';
import { player } from './player';
export const commonSchema = pgSchema('common');
export const gmHost = commonSchema.table('gm_host', {
ip: varchar({ length: 16 }).notNull(),
});
export const gmAuthority = commonSchema.enum('gm_authority', [
'IMPLEMENTOR',
'HIGH_WIZARD',
'GOD',
'LOW_WIZARD',
'PLAYER',
]);
export const gmList = commonSchema.table('gm_list', {
playerId: integer('player_id')
.notNull()
.references(() => player.id),
contactIp: varchar('contact_ip', { length: 16 }).notNull(),
serverIp: varchar('server_ip', { length: 16 }).default('ALL').notNull(),
authority: gmAuthority().default('PLAYER'),
});
export const locale = commonSchema.table(
'locale',
{
key: varchar({ length: 191 }).notNull(),
value: varchar({ length: 191 }).notNull(),
},
(table) => [index('locale_key').on(table.key)],
);
export const spamType = commonSchema.enum('spam_type', ['GOOD', 'SPAM']);
export const spamDb = commonSchema.table('spam_db', {
type: spamType().notNull().default('SPAM'),
word: varchar({ length: 256 }).notNull(),
score: integer().default(10).notNull(),
});

5
src/db/schema/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './account';
export * from './common';
export * from './log';
export * from './player';
export * from './web';

301
src/db/schema/log.ts Normal file
View File

@ -0,0 +1,301 @@
import {
timestamp,
char,
integer,
varchar,
text,
index,
boolean,
pgSchema,
} from 'drizzle-orm/pg-core';
import { account } from './account';
import { player } from './player';
export const logSchema = pgSchema('log');
export const bootLog = logSchema.table('boot_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
hostname: char({ length: 128 }).default('UNKNOWN').notNull(),
channel: integer().default(0).notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const changeName = logSchema.table('change_name', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
oldName: varchar('old_name', { length: 191 }),
newName: varchar('new_name', { length: 191 }),
ip: varchar({ length: 16 }),
createdAt: timestamp('created_at').defaultNow(),
});
export const commandLog = logSchema.table('command_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
server: integer().notNull(),
ip: varchar({ length: 16 }).notNull(),
port: integer().notNull(),
username: varchar({ length: 50 }).notNull(),
command: text().notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const cubeLog = logSchema.table(
'cube_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id').notNull(),
x: integer().default(0).notNull(),
y: integer().default(0).notNull(),
itemVnum: integer('item_vnum').default(0).notNull(),
itemUid: integer('item_uid').default(0).notNull(),
itemCount: integer('item_count').default(0).notNull(),
success: boolean().notNull(),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [
index('cube_log_player_id').on(table.playerId),
index('cube_log_item_vnum').on(table.itemVnum),
index('cube_log_item_uid').on(table.itemUid),
],
);
export const dragonSlayLog = logSchema.table('dragon_slay_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
guildId: integer('guild_id').notNull(),
vnum: integer().notNull(),
startAt: timestamp('start_time').defaultNow().notNull(),
endTime: timestamp('end_time').defaultNow().notNull(),
});
export const fishLog = logSchema.table('fish_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
mapIndex: integer('map_index').default(0).notNull(),
fishId: integer('fish_id').default(0).notNull(),
fishingLevel: integer('fishing_level').default(0).notNull(),
waitingTime: integer('waiting_time').default(0).notNull(),
success: boolean().notNull(),
size: integer().notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const goldLogHow = logSchema.enum('gold_log_how', [
'BUY',
'SELL',
'SHOP_SELL',
'SHOP_BUY',
'EXCHANGE_TAKE',
'EXCHANGE_GIVE',
'QUEST',
]);
export const goldLog = logSchema.table(
'gold_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
what: integer().default(0).notNull(),
how: goldLogHow().notNull(),
hint: varchar({ length: 50 }),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [
index('gold_log_created_at').on(table.createdAt),
index('gold_log_player_id').on(table.playerId),
index('gold_log_what').on(table.what),
index('gold_log_how').on(table.how),
],
);
export const hackLog = logSchema.table('hack_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
login: char({ length: 16 }).notNull(),
name: char({ length: 24 }).notNull(),
ip: char({ length: 15 }).notNull(),
server: char({ length: 100 }).notNull(),
why: char({ length: 191 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const levelLog = logSchema.table('level_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: char({ length: 24 }).notNull(),
level: integer().default(0).notNull(),
playtime: integer().default(0).notNull(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
createdAt: timestamp('created_at').defaultNow(),
});
export const logType = logSchema.enum('log_type', ['ITEM', 'CHARACTER']);
export const log = logSchema.table(
'log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
type: logType().default('ITEM').notNull(),
who: integer().default(0).notNull(),
x: integer().default(0).notNull(),
y: integer().default(0).notNull(),
what: integer().default(0).notNull(),
how: varchar({ length: 50 }).notNull(),
hint: varchar({ length: 70 }),
ip: varchar({ length: 16 }),
vnum: integer(),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [
index('log_who').on(table.who),
index('log_what').on(table.what),
index('log_how').on(table.how),
],
);
export const loginLogType = logSchema.enum('login_log_type', [
'LOGIN',
'LOGOUT',
]);
export const loginLog = logSchema.table(
'login_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
type: loginLogType(),
channel: integer(),
accountId: integer('account_id')
.notNull()
.references(() => account.id),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
level: integer(),
job: integer(),
playtime: integer(),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [index('login_log_player_id').on(table.playerId, table.type)],
);
export const loginLog2 = logSchema.table('login_log_2', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
type: text(),
isGm: boolean('is_gm'),
channel: integer(),
accountId: integer('account_id')
.notNull()
.references(() => account.id),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
ip: text(),
clientVersion: text('client_version'),
playtime: integer().default(0).notNull(),
loginAt: timestamp('login_at'),
logoutAt: timestamp('logout_at'),
});
export const moneyLogType = logSchema.enum('money_log_type', [
'MONSTER',
'SHOP',
'REFINE',
'QUEST',
'GUILD',
'MISC',
'KILL',
'DROP',
]);
export const moneyLog = logSchema.table(
'money_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
type: moneyLogType(),
vnum: integer().default(0).notNull(),
gold: integer().default(0).notNull(),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [index('money_log_type').on(table.type, table.vnum)],
);
export const playerCount = logSchema.table('player_count', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
countRed: integer('count_red'),
countYellow: integer('count_yellow'),
countBlue: integer('count_blue'),
countTotal: integer('count_total'),
createdAt: timestamp('created_at').defaultNow(),
});
export const rewardType = logSchema.enum('reward_type', ['EXP', 'ITEM']);
export const questRewardLog = logSchema.table(
'quest_reward_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
questName: varchar('quest_name', { length: 32 }),
playerId: integer('player_id').notNull(),
playerLevel: integer('player_level'),
rewardType: rewardType('reward_type'),
rewardValue1: integer('reward_value1'),
rewardValue2: integer('reward_value2'),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [index('quest_reward_log_player_id').on(table.playerId)],
);
export const refineLogSetType = logSchema.enum('refine_log_set_type', [
'SOCKET',
'POWER',
'ROD',
'GUILD',
'SCROLL',
'HYUNIRON',
'GOD_SCROLL',
'MUSIN_SCROLL',
]);
export const refineLog = logSchema.table('refine_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
itemName: varchar('item_name', { length: 24 }).notNull(),
itemId: integer('item_id').default(0).notNull(),
step: varchar({ length: 50 }).notNull(),
isSuccess: boolean('is_success').notNull(),
setType: refineLogSetType('set_type').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const shoutLog = logSchema.table(
'shout_log',
{
id: integer().primaryKey().generatedAlwaysAsIdentity(),
channel: integer(),
empire: integer(),
shout: varchar({ length: 350 }),
createdAt: timestamp('created_at').defaultNow(),
},
(table) => [index('shout_log_created_at').on(table.createdAt)],
);
export const speedHackLog = logSchema.table('speed_hack_log', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
playerId: integer('player_id')
.notNull()
.references(() => player.id),
x: integer(),
y: integer(),
hackCount: integer('hack_count'),
createdAt: timestamp('created_at'),
});

1135
src/db/schema/player.ts Normal file

File diff suppressed because it is too large Load Diff

89
src/db/schema/web.ts Normal file
View File

@ -0,0 +1,89 @@
import {
integer,
jsonb,
pgSchema,
text,
timestamp,
varchar,
} from 'drizzle-orm/pg-core';
import { account } from './account';
export const webSchema = pgSchema('web');
export const session = webSchema.table('session', {
id: text().primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => account.id),
ip: varchar({ length: 16 }).notNull().default('0.0.0.0'),
expiresAt: timestamp('expires_at', {
withTimezone: true,
mode: 'date',
}).notNull(),
});
export const passwordResetSession = webSchema.table('password_reset_session', {
id: text().primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => account.id),
email: text().notNull(),
code: text().notNull(),
expiresAt: timestamp('expires_at', {
withTimezone: true,
}).notNull(),
});
export const download = webSchema.table('download', {
id: integer().primaryKey().generatedByDefaultAsIdentity(),
label: text().notNull(),
url: text().notNull(),
});
export const ticketCategory = webSchema.table('ticket_category', {
id: integer().primaryKey().generatedByDefaultAsIdentity(),
label: text().notNull(),
description: text(),
publishedAt: timestamp('published_at'),
});
const AttachmentType = ['image', 'video', 'document', 'other'];
interface AttachmentInterface {
preview?: string;
size: number;
name: string;
type: (typeof AttachmentType)[number];
url: string;
}
// TODO Fully figure out the attachment setup
export const ticket = webSchema.table('ticket', {
id: integer().primaryKey().generatedByDefaultAsIdentity(),
userId: integer()
.notNull()
.references(() => account.id),
categoryId: integer()
.notNull()
.references(() => ticketCategory.id),
title: text().notNull(),
details: text().notNull(),
attachments: jsonb().notNull().default([]).$type<AttachmentInterface[]>(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at'),
closedAt: timestamp('closed_at'),
});
export const ticketComment = webSchema.table('ticket_comment', {
id: integer().primaryKey().generatedByDefaultAsIdentity(),
userId: integer()
.notNull()
.references(() => account.id),
ticketId: integer()
.notNull()
.references(() => ticket.id),
comment: text().notNull(),
attachments: jsonb().notNull().default([]).$type<AttachmentInterface[]>(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at'),
});

View File

@ -0,0 +1,81 @@
import env from '@/env';
import {
Body,
Button,
Container,
Head,
Html,
Img,
Preview,
render,
Section,
Tailwind,
Text,
} from '@react-email/components';
interface DropboxResetPasswordEmailProps {
username: string;
link: string;
}
export const ResetPasswordEmail = ({
username = 'wildego',
link = env.APP_URL,
}: DropboxResetPasswordEmailProps) => {
return (
<Html>
<Head />
<Tailwind config={{
theme: {
colors: {
primary: 'oklch(0.205 0 0)',
'primary-foreground': 'oklch(0.985 0 0)',
black: '#000000',
border: 'oklch(0.922 0 0)'
}
}
}}>
<Body className='-auto my-auto bg-white px-2 font-sans'>
<Preview>{env.APP_NAME} reset your password</Preview>
<Container className='mx-auto my-[40px] max-w-[465px] rounded border border-border border-solid p-[20px]'>
<Img
src={`${env.APP_URL}/static/logo.png`}
width="155"
height="39"
alt={env.APP_NAME}
className="mx-auto my-0"
/>
<Section>
<Text className="text-[14px] text-black leading-[24px]">Hi {username},</Text>
<Text className="text-[14px] text-black leading-[24px]">
Someone recently requested a password change for your {env.APP_NAME}{' '}
account. If this was you, you can set a new password here:
</Text>
</Section>
<Section>
<Button className="rounded-md text-[14px] font-medium bg-primary text-primary-foreground shadow-xs px-5 py-3 no-underline" href={link}>
Reset password
</Button>
</Section>
<Section>
<Text className="text-[14px] text-black leading-[24px]">
If you don&apos;t want to change your password or didn&apos;t
request this, just ignore and delete this message.
</Text>
<Text className="text-[14px] text-black leading-[24px]">
To keep your account secure, please don&apos;t forward this email
to anyone.
</Text>
<Text className="text-[14px] text-black leading-[24px]">Regards, {env.APP_NAME}.</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default ResetPasswordEmail;
export const renderResetPasswordHtml = async (props: DropboxResetPasswordEmailProps) => await render(<ResetPasswordEmail {...props} />)
export const renderResetPasswordText = async (props: DropboxResetPasswordEmailProps) => await render(<ResetPasswordEmail {...props} />, {plainText: true})

26
src/env.ts Normal file
View File

@ -0,0 +1,26 @@
import { z } from 'zod/v4';
const schema = z.object({
APP_NAME: z.string(),
APP_URL: z.url(),
SESSION_KEY: z.string(),
DATABASE_URL: z.string(),
EMAIL_HOST: z.string(),
EMAIL_PORT: z.coerce.number(),
EMAIL_USE_SSL: z
.enum(['true', 'false'])
.transform((value) => value === 'true'),
EMAIL_USERNAME: z.string(),
EMAIL_PASSWORD: z.string(),
EMAIL_FROM: z.email(),
STORAGE_ENDPOINT: z.url(),
STORAGE_ACCESS_KEY_ID: z.string(),
STORAGE_ACCESS_KEY_SECRET: z.string(),
STORAGE_MALL_BUCKET: z.string(),
STORAGE_SUPPORT_BUCKET: z.string(),
});
export default schema.parse(process.env);

6
src/global-middleware.ts Normal file
View File

@ -0,0 +1,6 @@
import { registerGlobalMiddleware } from '@tanstack/react-start';
import { localeMiddleware } from './lib/i18n/locale-middleware';
registerGlobalMiddleware({
middleware: [localeMiddleware],
});

19
src/hooks/use-mobile.ts Normal file
View File

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

157
src/lib/actions/auth.ts Normal file
View File

@ -0,0 +1,157 @@
import { createServerFn } from '@tanstack/react-start';
import { useAppSession } from '../utils/session';
import { database } from '@/db';
import { createInsertSchema } from 'drizzle-zod';
import { account } from '@/db/schema';
import { z } from 'zod/v4';
import { redirect } from '@tanstack/react-router';
import {
createSession,
invalidateSession,
validateSessionToken,
} from '../session/auth';
import { getIp } from '../utils';
import { generateSessionToken } from '../session';
import { password } from 'bun';
import {
handleServerFnValidation,
serverError,
unauthorizedError,
validationError,
} from '../utils/http';
export const fetchUser = createServerFn({
method: 'GET',
response: 'data',
}).handler(async () => {
const session = await useAppSession();
if (!session.data.sessionToken) {
return { session: null, user: null };
}
return await validateSessionToken(session.data.sessionToken, getIp());
});
export const RegisterSchema = createInsertSchema(account)
.pick({
login: true,
email: true,
socialId: true,
password: true,
})
.extend({
redirectUrl: z.optional(z.string()),
});
export const register = createServerFn({
method: 'POST',
response: 'data',
})
.validator((payload: z.infer<typeof RegisterSchema>) =>
handleServerFnValidation(RegisterSchema, payload),
)
.handler(async ({ data }) => {
const [user] = await database
.insert(account)
.values({
login: data.login,
email: data.email,
socialId: data.socialId,
password: await password.hash(data.password, 'argon2id'),
})
.returning();
if (!user) {
throw serverError();
}
const appSession = await useAppSession();
const session = await createSession(
generateSessionToken(),
user.id,
getIp(),
);
await appSession.update({
sessionToken: session.id,
});
throw redirect({
href: data.redirectUrl || '/',
});
});
export const LoginSchema = createInsertSchema(account)
.pick({
login: true,
password: true,
})
.extend({
redirectUrl: z.optional(z.string()),
});
export const login = createServerFn({
method: 'POST',
response: 'data',
})
.validator((payload: z.infer<typeof LoginSchema>) =>
handleServerFnValidation(LoginSchema, payload),
)
.handler(async ({ data }) => {
const user = await database.query.account.findFirst({
where: {
login: {
eq: data.login,
},
},
});
if (!user) {
throw validationError({
login: ['Invalid credentials'],
});
}
if (!(await password.verify(data.password, user.password, 'argon2id'))) {
throw validationError({
login: ['Invalid credentials'],
});
}
const appSession = await useAppSession();
const session = await createSession(
generateSessionToken(),
user.id,
getIp(),
);
await appSession.update({
sessionToken: session.id,
});
throw redirect({
href: data.redirectUrl || '/',
});
});
export const logout = createServerFn({
method: 'POST',
response: 'data',
}).handler(async () => {
const appSession = await useAppSession();
if (!appSession.data.sessionToken) {
throw unauthorizedError();
}
await invalidateSession(appSession.data.sessionToken);
await appSession.clear();
throw redirect({
href: '/',
});
});

Some files were not shown because too many files have changed in this diff Show More