diff --git a/.env.example b/.env.example
index ba371c31f..492fb4788 100644
--- a/.env.example
+++ b/.env.example
@@ -6,7 +6,7 @@ SESSION_SECRET=secret
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
-// Patreon integration https://www.patreon.com/portal/registration/register-clients
+// Patreon integration to sync supporter status https://www.patreon.com/portal/registration/register-clients
PATREON_ACCESS_TOKEN=
// Image upload
@@ -17,6 +17,7 @@ STORAGE_REGION=
STORAGE_BUCKET=
STORAGE_URL=
+// Twitch integration for fetching streams
TWITCH_CLIENT_ID=
TWITCH_CLIENT_SECRET=
@@ -26,15 +27,15 @@ SKALOP_TOKEN=secret
VITE_SITE_DOMAIN=http://localhost:5173
VITE_SKALOP_WS_URL=ws://localhost:5900
-// if true uses real seasons & league data
+// If true uses real seasons & league data. Used when developing with the production database
VITE_PROD_MODE=false
-// trunc, full or none (default: none)
-SQL_LOG=trunc
+// Whether to log SQL queries made via Kysely to console. Possible values: trunc, full or none (default: none)
+SQL_LOG=none
VITE_SHOW_LUTI_NAV_ITEM=false
-// generate here https://vapidkeys.com/
+// Push notification. Generate values here https://vapidkeys.com/
VITE_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_EMAIL=
diff --git a/.gitignore b/.gitignore
index a0f89c5e1..234960f4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,4 @@ dump
/playwright-report/
/playwright/.cache/
-.vscode
-
notepad.txt
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..3e98c9519
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["bierner.markdown-mermaid", "biomejs.biome"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..4f375904f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "editor.defaultFormatter": "biomejs.biome"
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..8068718f4
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
+at [https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6757a1dc2..9eba6d4e9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,27 +2,29 @@
## How to contribute
-### Creating issues
+Reading the [architecture.md](./docs/dev/architecture.md) file is highly recommended before writing any code to get up to the speed with how the project folder structure works and getting familiar with its concepts.
-You are free to create issues about bugs, feature requests or just questions about the project. Try to include all relevant information e.g. with bugs what browser you have tested and how to reproduce.
+### Bugs
-### Making pull request
+Feel free to submit issues and/or pull requests about bugs. In a bug issue try to include all the relevant information e.g. with bugs what browser you have tested and how to reproduce. Before making an issue use search to make sure one doesn't already exist.
+
+## Features
+
+The "product vision" of sendou.ink is controlled mainly by Sendou. Best way to gauge interest about a new feature is to post about it on the feedback channel of [our Discord](https://discord.gg/sendou). If it's a feature you personally want to implement you can also state that when opening a Discord topic about it for discussion. We can then work on the idea and create it into a GitHub issue before implementation. Before making a new post use the search functionality to look if one already exists (it's okay to bump it up).
+
+You can also just directly make a pull request but untracked feature requests might be rejected if they are not something that fit the vision. The fact that all new code needs maintaining needs to be weighted against how much it benefits the userbase as well as any connections to other features current or future.
+
+## Making pull request
1. Identify an issue to work on
- Issues I have identified as potentially good for external contributors [are marked with the help wanted tag.](https://github.com/Sendouc/sendou.ink/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
-- Alternatively if there is something else you want to work on (maybe a bug you found or a new feature) I recommend creating a new issue about it before making a pull request. This way I can give any pointers or identify any problems before you use time to make the pull request.
+- "good first issue" are tasks that might be smaller in scope and easier to get started with
-2. Leave a comment to the issue that you will be working on it.
-
-3. Make pull request
+2. Make pull request
- Tell me how I can test it and include unit / E2E tests if possible.
## Help
If you need help with anything related to contributing we have [a Discord server](https://discord.gg/sendou). Especially the "development" channel is meant for this.
-
-## Code of conduct
-
-[Contributor Covenant](https://www.contributor-covenant.org/) applies.
diff --git a/README.md b/README.md
index 90a14b0f4..e1212121d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,22 @@
-sendou.ink
-Competitive Splatoon Platform
+sendou.ink - a Splatoon platform with competitive focus
-## Selected Features
+What differentiates sendou.ink from some other gaming platforms (e.g. tournament hosting platforms) is its 100% focus on Splatoon. This allows for more custom tailored experience around the game and the community. The wide range of distinct features make it possible to create seamless integrations between them further enriching the user experience beyond what external integrations would allow.
+
+Another key objective is to bridge the gap between casual and competitive players. For example, features like a gear build simulator can appeal not only to competitive players but also to those who simply want to enhance their gameplay without participating in tournaments. This allows sendou.ink to be useful for a wider audience but also provides a window to the competitive side of things for those that might be interested.
+
+
+Screenshots
+
+
+
+
+
+
+
+
+Selected features
- Full tournament system
- Automatic bracket progression
@@ -26,7 +39,8 @@ Competitive Splatoon Platform
- Plus Server for top players "looking for group purposes" voting and suggestion tools.
- User pages
- User search
-- "LFG", find make a post to find people to play with
+- "LFG", make a post to find people to play with
+- Scrim scheduler
- Form teams (featuring uploading profile and banner pictures)
- Object Damage Calculator (how much does each weapon deal vs. different objects)
- Build Analyzer (exact stats of your builds)
@@ -35,215 +49,64 @@ Competitive Splatoon Platform
- Light and dark mode
- Localization
-## Tech stack
-
-- React
-- Remix
-- Sqlite3
-- CSS (plain)
-- E2E tests via Playwright
-- Unit/integration tests via Vitest
-
-## Screenshots
-
-
-
-
-
----
+
## Running locally
-### sendou.ink
+### Prerequisites
-Prerequisites: [nvm](https://github.com/nvm-sh/nvm)
+- [Git](https://git-scm.com/)
+- [Node.js v22](https://nodejs.org/en)
-There is a sequence of commands you need to run:
+Optionally [nvm](https://github.com/nvm-sh/nvm) can be convenient for managing multiple Node.js installs
-1. `nvm use` to switch to the correct Node version. If you don't have the correct Node.js version yet it will prompt you to install it via the `nvm install` command. If you have problems with nvm you can also install the latest LTS version of Node.js from [their website](https://nodejs.org/en/).
-1. `npm install` to install the dependencies.
-1. Make a copy of `.env.example` that's called `.env`. Filling additional values is not necessary unless you want to use real Discord authentication or develop the Lohi bot.
-1. `npm run migrate up` to set up the database tables.
-1. `npm run dev` to run the project in development mode.
-1. Navigate to `http://localhost:5173/admin`. There press the seed button to fill the DB with test data. You can also impersonate any user (Sendou#0043 = admin).
+### Commands
+
+First verify you have Node.js and git installed:
+
+```bash
+node --version
+git --version
+```
+
+You should see something like:
+
+```
+v22.13.0
+git version 2.39.5 (Apple Git-154)
+```
+(if not then go back to "Prerequisites" and install what is missing)
+
+Then there is a sequence of commands you need to run:
+
+```bash
+git clone https://github.com/Sendouc/sendou.ink.git # Clones repository
+cd sendou.ink # Change to the project's folder
+npm install # Install dependencies
+npm run dev # Setup the development environment and run the project
+```
+
+You should then be able to access the application by visiting http://localhost:5173
+
+Use the admin panel at http://localhost:5173/admin to log in (impersonate) as the admin user "Sendou" or as a regular user "N-ZAP" as well as re-seed the database if needed.
## Contributing
-See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.
+- **Developers**: Read [CONTRIBUTING.md](./CONTRIBUTING.md)
+- **Translation**: Read [translation.md](./docs/translation.md)
+- **Article writing**: Read [articles.md](./docs/articles.md)
-## Tests
+For developers reading the [architecture.md](./docs/dev/architecture.md) file is highly recommended to get up to the speed with how the project folder structure works and getting familiar with its concepts.
-### `db-test.sqlite3`
+## Tech stack
-Empty DB with the latest migration run. When creating new migrations they should also be applied+committed to this file (add it in `.env` and then run the migration command as normal).
-
-### Translations
-
-[Translation Progress](https://github.com/Sendouc/sendou.ink/issues/1104)
-
-sendou.ink can be translated into any language. All the translations can be found in the [locales folder](./locales). Here is how you can contribute:
-
-1. Copy a `.json` file from `/en` folder.
-2. Translate lines one by one. For example `"country": "Country",` could become `"country": "Maa",`. Keep the "key" on the left side of : unchanged.
-3. Finally, send the translated .json to Sendou or make a pull request if you know how.
-
-Things to note:
-
-- `weapons.json` and `gear.json` are auto-generated. Don't touch these.
-- If some language doesn't have a folder it can be added.
-- Some translated `.json` files can also have some lines in English as new lines get added to the site. Those can then be translated.
-- Some lines have a dynamic part like this one: `"articleBy": "by {{author}}"` in this case `{{author}}` should appear in the translated version unchanged. So in other words don't translate the part inside `{{}}`.
-- There is one more special syntax to keep in mind. When you translate this line `"project": "Sendou.ink is a project by <2>Sendou2> with help from contributors:",` the `<2>2>` should appear in the translated version. The text inside these tags can change.
-- To update a translation file copy the existing file, do any modifications needed and send the updated one.
-
-Any questions please ask Sendou!
-
-### Articles
-
-1. Take an existing article as a base: https://raw.githubusercontent.com/Sendouc/sendou.ink/rewrite/content/articles/in-the-zone-26-winners.md
-2. Copy and paste the contents to a text file
-3. First, edit the info section at the top:
-
-- "title" = title of the page
-- "date" = date when this article was written (format YYYY-MM-DD)
-- "author" = your name as you want it shown on the website
-
-4. Write the actual article below the second `---`
-5. You can use Markdown for more advanced formatting (read https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax for more info)
-6. Name the file as you want the URL to be. For example `in-the-zone-26-winners.md` becomes `https://sendou.ink/a/in-the-zone-26-winners`
-7. Send the file to Sendou (or open a pull request if you know how)
-8. Optional: also send an image as .png if you want to show a link preview. The preferred dimensions are 1200 × 630.
-
-## SQL Logging
-
-By default SQL is logged in truncated format. You can adjust this by changing the `SQL_LOG` env var. Possible values are "trunc", "full" and "none".
-
-Note it only logs queries made via Kysely.
-
-## API
-
-If you want to use the API then please leave an issue explaining your use case. By default, I want to allow open use of the data on the site. It's just not recommended to use the same APIs the web pages use as they are not stable at all and can change at any time without warning.
-
-## Project structure
-
-```
-sendou.ink/
-├── app/
-│ ├── components/ -- React components
-│ ├── db/ -- Database layer
-│ ├── hooks/ -- React hooks
-│ ├── modules/ -- "node_modules but part of the app"
-│ ├── routes/ -- Routes see: https://remix.run/docs/en/v1/guides/routing
-│ ├── styles/ -- All .css files of the project for styling
-│ ├── utils/ -- Random helper functions used in many places
-│ └── permissions.ts / -- What actions are allowed. Separated by frontend and backend as frontend has constraints based on what user sees.
-├── migrations/ -- Database migrations
-├── public/ -- Images, built assets etc. static files to be served as is
-└── scripts/ -- Stand-alone scripts to be run outside of the app
-```
-
-NOTE: `public/static-assets` should only have files that don't change as it is cached for 1 month.
-
-### Feature folders
-
-Feature folders contain all the code needed to make that feature happen. Some common folders include:
-
-- routes (same principle as Remix file system routing)
-- queries
-- components
-- core (all core logic, separated from any React details)
-
-Some common files:
-
-- styles.css
-- feature-hooks.ts
-- feature-utils.ts
-- feature-constants.ts
-- feature-schemas.server.ts
-
-## Commands
-
-### Add new badge to the database
-
-```bash
-npx tsx scripts/add-badge.ts fire_green "Octofin Eliteboard"
-```
-
-### Rename display name of a badge
-
-```bash
-npx tsx scripts/rename-badge.ts 10 "New 4v4 Sundaes"
-```
-
-### Add many badge owners
-
-```bash
-npx tsx scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
-```
-
-### Converting gifs (badges) to thumbnail (.png)
-
-```bash
-sips -s format png ./sundae.gif --out .
-```
-
-### Convert many .png files to .avif
-
-While in the folder with the images:
-
-```bash
-for i in *.png; do npx @squoosh/cli --avif '{"cqLevel":33,"cqAlphaLevel":-1,"denoiseLevel":0,"tileColsLog2":0,"tileRowsLog2":0,"speed":6,"subsample":1,"chromaDeltaQ":false,"sharpness":0,"tune":0}' $i; done
-```
-
-Note: it only works with Node 16.
-
-## How to...
-
-### Download the production database from Render.com
-
-Note: This is only useful if you have access to a production running on Render.com
-
-1. Access the "Shell" tab
-2. `cd /var/data`
-3. `cp db.sqlite3 db-copy.sqlite3`
-4. `wormhole send db-copy.sqlite3`
-5. On the receiving computer use the command shown.
-
-### Doing monthly update
-
-1. Fill /scripts/dicts with new data from leanny repository:
- - weapon = contents of `weapon` folder
- - langs = contents of `language` folder
- - Couple of others at the root: `GearInfoClothes.json`, `GearInfoHead.json`, `GearInfoShoes.json`, `spl__DamageRateInfoConfig.pp__CombinationDataTableData.json`, `SplPlayer.game__GameParameterTable.json`, `WeaponInfoMain.json`, `WeaponInfoSpecial.json` and `WeaponInfoSub.json`
-1. Update all `CURRENT_SEASON` constants
-1. Update `CURRENT_PATCH` constants
-1. Update `PATCHES` constant with the late patch + remove the oldest
-1. Update the stage list in `stage-ids.ts` and `create-misc-json.ts`. Add images from Lean's repository and avify them.
-1. `npx tsx scripts/create-misc-json.ts`
-1. `npx tsx scripts/create-gear-json.ts`
-1. `npx tsx scripts/create-analyzer-json.ts`
- 8a. Double check that no hard-coded special damages changed
-1. `npx tsx scripts/create-object-dmg-json.ts`
-1. Fill new weapon IDs by category to `weapon-ids.ts` (easy to take from the diff of English weapons.json)
-1. Get gear IDs for each slot from /output folder and update `gear-ids.ts`.
-1. Replace `object-dmg.json` with the `object-dmg.json` in /output folder
-1. Replace `weapon-params.ts` with the `params.json` in /output folder
-1. Delete all images inside `main-weapons`, `main-weapons-outlined`, `main-weapons-outlined-2` and `gear` folders.
-1. Replace with images from Lean's repository.
-1. Run the `npx tsx scripts/replace-img-names.ts` command
-1. Run the `npx tsx scripts/replace-weapon-names.ts` command
-1. Run the .avif generating command in each image folder.
-2. Update manually any languages that use English `gear.json` and `weapons.json` files
-
-### Fix errors from the CI Pipeline
-
-If you change any files and the CI pipeline errors out on certain formatting/linting steps (Biome) run this command in the repo's root directory:
-
-```sh
-npm run checks
-```
-
-Before committing, if for some reason you see an abnormally high amount of files changed, simply run `git add --renormalize .` and it will fix the error.
-
-- Background info: this is caused by the line endings on your local repo not matching those with the remote repos, which should remove the vast majority of unstaged files that appears to have no changes at all.
-- Reference: https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
+- **Language**: TypeScript
+- **Frameworks**: Node.js, React, Remix
+- **UI Library**: React Aria Components
+- **Database**: SQLite3 (via Kysely)
+- **Styling**: CSS Modules
+- **Validation**: Zod
+- **Internationalization**: i18next
+- **Testing**:
+ - End-to-End (E2E): Playwright
+ - Unit/Integration: Vitest
diff --git a/app/db/tables.ts b/app/db/tables.ts
index 581e01469..08dce1be1 100644
--- a/app/db/tables.ts
+++ b/app/db/tables.ts
@@ -30,6 +30,9 @@ type Generated = T extends ColumnType
export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number];
+/** In SQLite booleans are presented as 0 (false) and 1 (true) */
+export type DBBoolean = number;
+
export interface Team {
avatarImgId: number | null;
bannerImgId: number | null;
@@ -52,7 +55,7 @@ export interface TeamMember {
role: MemberRole | null;
teamId: number;
userId: number;
- isMainTeam: number;
+ isMainTeam: DBBoolean;
}
export interface Art {
@@ -61,7 +64,7 @@ export interface Art {
description: string | null;
id: GeneratedAlways;
imgId: number;
- isShowcase: Generated;
+ isShowcase: Generated;
}
export interface ArtTag {
@@ -76,6 +79,11 @@ export interface ArtUserMetadata {
userId: number;
}
+export interface TaggedArt {
+ artId: number;
+ tagId: number;
+}
+
export interface Badge {
id: GeneratedAlways;
code: string;
@@ -102,7 +110,7 @@ export interface Build {
id: GeneratedAlways;
modes: JSONColumnTypeNullable;
ownerId: number;
- private: number | null;
+ private: DBBoolean | null;
shoesGearSplId: number;
title: string;
updatedAt: Generated;
@@ -140,7 +148,7 @@ export interface CalendarEvent {
name: string;
participantCount: number | null;
tags: string | null;
- hidden: Generated;
+ hidden: Generated;
tournamentId: number | null;
organizationId: number | null;
avatarImgId: number | null;
@@ -191,7 +199,7 @@ export interface GroupLike {
createdAt: Generated;
likerGroupId: number;
targetGroupId: number;
- isRechallenge: number | null;
+ isRechallenge: DBBoolean | null;
}
type CalculatingSkill = {
@@ -276,6 +284,7 @@ export interface PrivateUserNote {
updatedAt: Generated;
}
+/** Log-in links generated via the Lohi Discord bot commands. */
export interface LogInLink {
code: string;
expiresAt: number;
@@ -329,12 +338,6 @@ export interface MapResult {
wins: number;
}
-export interface Migrations {
- created_at: string;
- id: GeneratedAlways;
- name: string;
-}
-
export interface PlayerResult {
mapLosses: number;
mapWins: number;
@@ -378,8 +381,8 @@ export interface PlusVotingResult {
score: number;
month: number;
year: number;
- wasSuggested: number;
- passedVoting: number;
+ wasSuggested: DBBoolean;
+ passedVoting: DBBoolean;
}
export interface ReportedWeapon {
@@ -406,6 +409,7 @@ export interface SkillTeamUser {
userId: number;
}
+/** Used for tournament auto-seeding. Calculates off tournament matches same as SP but does not have seasonal resets. */
export interface SeedingSkill {
mu: number;
ordinal: number;
@@ -544,6 +548,7 @@ export interface TournamentMatch {
createdAt: Generated;
}
+/** Represents one decision, pick or ban, during tournaments pick/ban (counterpick, ban 2) phase. */
export interface TournamentMatchPickBanEvent {
type: "PICK" | "BAN";
stageId: StageId;
@@ -640,6 +645,7 @@ export interface TournamentStage {
createdAt: number | null;
}
+/** Tournament sub post, shown in a list of subs available for teams to pick from. */
export interface TournamentSub {
bestWeapons: string;
/** 0 = no, 1 = yes, 2 = listen only */
@@ -663,9 +669,9 @@ export interface TournamentTeam {
id: GeneratedAlways;
inviteCode: string;
name: string;
- prefersNotToHost: Generated;
- noScreen: Generated;
- droppedOut: Generated;
+ prefersNotToHost: Generated;
+ noScreen: Generated;
+ droppedOut: Generated;
seed: number | null;
/** For formats that have many starting brackets, where should the team start? */
startingBracketIdx: number | null;
@@ -738,6 +744,7 @@ export interface TournamentBracketProgressionOverride {
tournamentId: number;
}
+/** Indicates a user trusts another. Allows direct adding to groups/teams without invite links. */
export interface TrustRelationship {
trustGiverUserId: number;
trustReceiverUserId: number;
@@ -748,6 +755,7 @@ export interface UnvalidatedUserSubmittedImage {
id: GeneratedAlways;
submitterUserId: number;
url: string;
+ /** When was the image validated? If `null` should be hidden from other users. */
validatedAt: number | null;
}
@@ -767,6 +775,7 @@ export type Preference = "AVOID" | "PREFER";
export interface UserMapModePreferences {
modes: Array<{
mode: ModeShort;
+ /** Users opinion on the mode, `undefined` means neutral */
preference?: Preference;
}>;
pool: Array<{
@@ -823,15 +832,15 @@ export interface User {
favoriteBadgeIds: ColumnType;
id: GeneratedAlways;
inGameName: string | null;
- isArtist: Generated;
- isVideoAdder: Generated;
- isTournamentOrganizer: Generated;
+ isArtist: Generated;
+ isVideoAdder: Generated;
+ isTournamentOrganizer: Generated;
languages: string | null;
motionSens: number | null;
patronSince: number | null;
patronTier: number | null;
patronTill: number | null;
- showDiscordUniqueName: Generated;
+ showDiscordUniqueName: Generated;
stickSens: number | null;
twitch: string | null;
bsky: string | null;
@@ -841,7 +850,7 @@ export interface User {
mapModePreferences: JSONColumnTypeNullable;
qWeaponPool: JSONColumnTypeNullable;
plusSkippedForSeasonNth: number | null;
- noScreen: Generated;
+ noScreen: Generated;
buildSorting: JSONColumnTypeNullable;
preferences: JSONColumnTypeNullable;
}
@@ -865,7 +874,7 @@ export interface UserSubmittedImage {
export interface UserWeapon {
createdAt: Generated;
- isFavorite: Generated;
+ isFavorite: Generated;
order: number;
userId: number;
weaponSplId: MainWeaponId;
@@ -953,15 +962,15 @@ export interface ScrimPostRequest {
id: GeneratedAlways;
scrimPostId: number;
teamId: number | null;
- isAccepted: Generated;
+ isAccepted: Generated;
createdAt: GeneratedAlways;
}
export interface ScrimPostRequestUser {
scrimPostRequestId: number;
+ /** User that made the request */
userId: number;
- /** User made the request */
- isOwner: number;
+ isOwner: DBBoolean;
}
export interface Association {
@@ -988,7 +997,7 @@ export interface Notification {
export interface NotificationUser {
notificationId: number;
userId: number;
- seen: Generated;
+ seen: Generated;
}
export interface NotificationSubscription {
@@ -999,6 +1008,7 @@ export interface NotificationSubscription {
};
}
+/** A subscription of user's browser indicating where push notifications can be sent to. */
export interface NotificationUserSubscription {
id: GeneratedAlways;
userId: number;
@@ -1015,9 +1025,11 @@ export interface DB {
Art: Art;
ArtTag: ArtTag;
ArtUserMetadata: ArtUserMetadata;
+ TaggedArt: TaggedArt;
Badge: Badge;
BadgeManager: BadgeManager;
BadgeOwner: BadgeOwner;
+ TournamentBadgeOwner: TournamentBadgeOwner;
Build: Build;
BuildAbility: BuildAbility;
BuildWeapon: BuildWeapon;
@@ -1047,13 +1059,11 @@ export interface DB {
SkillTeamUser: SkillTeamUser;
SeedingSkill: SeedingSkill;
SplatoonPlayer: SplatoonPlayer;
- TaggedArt: TaggedArt;
Team: Team;
TeamMember: TeamMember;
TeamMemberWithSecondary: TeamMember;
Tournament: Tournament;
TournamentStaff: TournamentStaff;
- TournamentBadgeOwner: TournamentBadgeOwner;
TournamentGroup: TournamentGroup;
TournamentMatch: TournamentMatch;
TournamentMatchPickBanEvent: TournamentMatchPickBanEvent;
diff --git a/docs/articles.md b/docs/articles.md
new file mode 100644
index 000000000..762a0fe68
--- /dev/null
+++ b/docs/articles.md
@@ -0,0 +1,15 @@
+# Articles
+
+1. Take an existing article as a base: https://raw.githubusercontent.com/Sendouc/sendou.ink/rewrite/content/articles/in-the-zone-26-winners.md
+2. Copy and paste the contents to a text file
+3. First, edit the info section at the top:
+
+- "title" = title of the page
+- "date" = date when this article was written (format YYYY-MM-DD)
+- "author" = your name as you want it shown on the website
+
+4. Write the actual article below the second `---`
+5. You can use Markdown for more advanced formatting (read https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax for more info)
+6. Name the file as you want the URL to be. For example `in-the-zone-26-winners.md` becomes `https://sendou.ink/a/in-the-zone-26-winners`
+7. Send the file to Sendou (or open a pull request if you know how)
+8. Optional: also send an image as .png if you want to show a link preview. The preferred dimensions are 1200 × 630.
diff --git a/docs/dev/api.md b/docs/dev/api.md
new file mode 100644
index 000000000..e35c9bd20
--- /dev/null
+++ b/docs/dev/api.md
@@ -0,0 +1,18 @@
+# API
+
+API for external projects to access sendou.ink data for projects such as streams is available. This API is for reading data, writing is not supported. You will need a token to access the API. Currently access is limited but you can request a token from Sendou.
+
+## Endpoints
+
+Check out `sendou.ink/app/features/api-public/schema.ts`
+
+## Curl example
+
+```bash
+sendou@macbook ~ % curl -H "Authorization: Bearer mytoken" https://sendou.ink/api/tournament/1
+{"name":"PICNIC mini","startTime":"2023-05-18T18:00:00.000Z","url":"https://sendou.ink/to/1/brackets","logoUrl":"https://sendou.ink/static-assets/img/tournament-logos/pn.png","teams":{"checkedInCount":25,"registeredCount":31},"brackets":[{"name":"Main bracket","type":"double_elimination"}],"organizationId":1,"isFinalized":true}%
+```
+
+## Clients (unofficial)
+
+- [sendou.py (Python)](https://github.com/IPLSplatoon/sendou.py)
diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md
new file mode 100644
index 000000000..3211ac150
--- /dev/null
+++ b/docs/dev/architecture.md
@@ -0,0 +1,201 @@
+# Architecture
+
+Note: some code in the project is older and some newer. Not everything follows the concepts and structure as explained here. PR's welcome to improve the situation.
+
+## Diagram
+
+Here is how the application architecture looks like in production.
+
+```mermaid
+graph TD
+ subgraph Render
+ A[sendou.ink Server] -->|Reads/Writes| B[SQLite3 Database]
+ A -->|HTTP Requests| E[Skalop WebSocket Server]
+ D[Lohi Discord Bot] -->|HTTP Requests| A
+ end
+
+ subgraph DigitalOcean
+ C[S3-Compatible Image Hosting]
+ end
+
+ F[User] -->|HTTP & WS| G[Cloudflare]
+ G -->|HTTP| A
+ G -->|WebSocket| E
+
+ A -->|S3 Upload| C
+ F -->|Views images| C
+
+```
+
+List of the dependencies in production:
+
+- [Skalop](https://github.com/Sendouc/skalop) - WebSocket server
+- [Lohi](https://github.com/Sendouc/lohi) - Discord bot for profile updates, log-in links etc.
+- [Leanny/splat3](https://github.com/Leanny/splat3) - In-game data (manual update)
+- [splatoon3.ink](https://github.com/misenhower/splatoon3.ink) - X Rank placement data (manual update)
+- Discord - Auth
+- Twitch - Streams
+- Bluesky - Front page changelog
+
+## Folder structure
+
+```
+sendou.ink/
+├── app/
+│ ├── components/ -- React components used by many features
+│ │ └── elements/ -- Wrappers providing styling etc. around React Aria Components
+│ ├── db/ -- Database seeds, types & connection
+│ ├── features/ -- See "feature folders" below
+│ ├── hooks/ -- React hooks used by many features
+│ ├── modules/ -- "node_modules but part of the app"
+│ ├── styles/ -- Global .css files
+│ ├── utils/ -- Helper functions grouped by domain used by many features
+│ ├── entry.client.tsx -- Client entry point (Remix concept)
+│ ├── entry.server.tsx -- Server entry point (Remix concept)
+│ ├── root.tsx -- Basic HTML structure, React context providers & root data loader
+│ └── routes.ts -- Route manifest
+├── content/ -- Markdown files containing articles
+├── docs/ -- Documentation to developers and users
+├── e2e/ -- Playwright tests
+├── locales/ -- Translation files
+├── migrations/ -- Database migrations
+├── public/ -- Images, built assets etc. static files to be served as is
+├── scripts/ -- Stand-alone scripts to be run outside of the app (i.e. not imported)
+└── types/ -- "global" type overwrites
+```
+
+## Feature folders
+
+Feature folders collect together all the code needed to make that particular feature happen: database, backend, frontend, core logic etc. Feature can mean an user facing feature like "map-planner" but also something of a more cross-cutting concern like "chat".
+
+You should aim to colocate code that "changes together" as much as possible. Features can depend (import) on other features.
+
+### Feature folder files & folders
+
+- **actions/**: Remix actions per route
+- **components/**: React components
+- **core/**: "Core logic" meaning modules (see below) or other logic that is not typically rendering components or calling database
+- **queries/**: (deprecated) Database queries, should use repository instead
+- **loaders/**: Remix loaders per route
+- **routes/**: Remix actions per route
+- **FeatureRepository.server.ts**: Database queries & mappers
+- **feature-constants.ts**: Constant values
+- **feature-hooks**: React hooks
+- **feature-schemas.ts**: Zod schemas for validating form values, params, payloads
+- **feature-types.ts**: Typescript types
+- **feature-utils.ts**: Utilities too small to make up for their own modules
+- **feature.css**: (deprecated) CSS, should use CSS modules instead
+
+Note: we are not using file-based routing. To add a new route `routes.ts` needs to be updated
+Note: a route file needs to re-export the action/loader of that route
+
+### Feature modules
+
+Define in a core folder:
+
+```ts
+// app/features/cool-feature/core/Module.ts
+
+/** Descriptive JSDoc goes here */
+export function doTheThing() {
+
+}
+function implementationDetail() {
+
+}
+```
+
+You should document any functions exported by the module well.
+
+Usage:
+
+```ts
+// anywhere else in the codebase, particularly inside that feature
+import * as Module from "../core/Module.ts"
+
+Module.doTheThing()
+```
+
+## Concepts
+
+### Testing
+
+Testing is important part of every feature work. The approach the project takes is pragmatic not super focused on writing test for every single thing but especially more mission critical features should have a better test coverage. E.g. if a tournament is canceled due to a bug that can mean a lot of lost confidence from users and waste of time but if some "edge of the system" type of feature has small graphical bugs we can just fix that on user feedback.
+
+Unit testing "core logic" (i.e. no React, no DB calls) with Vitest is highly encouraged whenever feasible. Most tests are like this.
+
+Vitest can also be used to write "integration tests" that call mocked actions/loaders (see `admin.test.ts` for example). This uses in-memory SQLite3. In practice this is best sparingly as they are typically slower than pure unit tests with more dependencies but also don't test the true end to end flow.
+
+Which brings us to E2E tests. For new features at least testing the happy path is encouraged. For more critical features (mainly tournament related stuff) it makes sense to test a bit more rigorously.
+
+See: [Playwright best practices](https://playwright.dev/docs/best-practices)
+
+### Authentication
+
+Accessing logged in user in React components:
+
+```tsx
+const user = useUser();
+```
+
+Accessing logged in user in loaders/actions:
+
+```ts
+const user = await requireUser(request); // get user or throw HTTP 401 if not logged in
+const user = await getUser(request); // get user (undefined if not logged in)
+```
+
+### Permissions
+
+1) Add a permission object in a `Repository` code.
+2) Read in React code via the `useHasPermission` hook.
+3) Read in server code via the `requirePermission` guard.
+
+User can also have global roles such as "staff" or "tournament adder". Set in the root loader and `getUser`/`requireUser` code.
+
+### Anatomy of an action
+
+TODO (after React server actions in use)
+
+### Performance
+
+Keeping server performance in mind is always necessary. Due to the monolithic nature of the server one badly optimized endpoint impacts all other routes.
+
+Use a load testing tool like `autocannon` to ensure new features scale.
+
+### Database
+
+Sendou.ink uses SQLite3 for its database solution. See for example ["Consider SQLite"](https://blog.wesleyac.com/posts/consider-sqlite) for motivation why to pick SQLite for a web project over something like PostgreSQL. Tldr; for a project of this scale it gets you far, low latency when accessing data store & simplifies testing when your database is just a file on the filesystem. When writing code it should be kept in mind that writes to the database are not concurrent so abusing the database can lead to the whole web server process freezing essentially.
+
+Check `database-relations.md` for more information about the database relations. See `tables.ts` for documentation on tables and columns.
+
+### React guidelines
+
+- Write modern React code as described by the documentation e.g. [seldom using useEffect](https://react.dev/learn/you-might-not-need-an-effect)
+- We use React Compiler so writing memos manually (useMemo, useCallback, React.memo) should normally not be needed
+- Structuring longer components to sub-components located in the same file is encouraged
+
+### State management
+
+We are not using a state management library such as Redux. Instead use React Context for the few global state needed and Remix's data loading hooks to share the state loaded from server. See also "Search params" section below.
+
+### Search params
+
+Often it's convenient to store state in search params. This allows for nice features like users to deep link to the view they are seeing. You have two options to achieve this:
+
+1) Use Remix's built-in solution. Use this if data loaders should rerun once search params are changed.
+2) `useSearchParamState` hook. Use this if it is not needed.
+
+### Routines
+
+Cron jobs to perform actions on the server at certain intervals. To add a new one, add a new file exporting an instance of the `Routine` class then add it to the appropriate array in the `app/routines/list.server.ts` file.
+
+### Real time
+
+Webhooks via Skalop service (see logic in the Chat module).
+
+Old way: server-sent events still in use for tournament bracket & match pages.
+
+### Notifications
+
+Both in-app and browser notifications. See `/app/features/notifications`. Good for notifying user about actions that they are interested in that might have happened when they are offline.
diff --git a/docs/dev/database-relations.md b/docs/dev/database-relations.md
new file mode 100644
index 000000000..0bbcab144
--- /dev/null
+++ b/docs/dev/database-relations.md
@@ -0,0 +1,200 @@
+Note: some simple features omitted with only a few relations and no special notes
+
+See `tables.ts` for some more documentation on column-level.
+
+## Art
+
+```mermaid
+erDiagram
+ Art ||--o{ ArtUserMetadata : has
+ User ||--o{ ArtUserMetadata : has
+
+ Art ||--o{ TaggedArt : tagged_with
+ ArtTag ||--o{ TaggedArt : tags
+
+ ArtTag ||--o{ User : created_by
+ Art ||--o{ User : created_by
+```
+
+## Badges
+
+```mermaid
+erDiagram
+ Badge ||--o{ BadgeManager : managed_by
+ User ||--o{ BadgeManager : manages
+
+ Badge ||--o{ TournamentBadgeOwner : owned_by
+ User ||--o{ TournamentBadgeOwner : owns
+
+ Badge }o--|| User : author
+```
+
+- **BadgeOwner** - Tournament badges with supporter badges included from user's supporter status
+
+## Builds
+
+```mermaid
+erDiagram
+ BuildAbility }|--|| Build : belongs_to
+ BuildWeapon }|--|| Build : belongs_to
+
+ Build }o--|| User : owned_by
+```
+
+## Calendar Events
+
+```mermaid
+erDiagram
+ CalendarEvent ||--o{ CalendarEventBadge : has
+ CalendarEventBadge }o--|| Badge : badge
+ CalendarEvent ||--|{ CalendarEventDate : has
+ CalendarEvent ||--o{ CalendarEventResultTeam : has
+ CalendarEventResultTeam ||--o{ CalendarEventResultPlayer : has
+ CalendarEvent }o--|| User : author
+ CalendarEvent }o--o| Organization : organized_by
+ CalendarEvent ||--o{ Tournament : related_to
+```
+
+### Notes
+
+- "Calendar event result" concept is only for tournaments not hosted on sendou.ink
+- Regular calendar event can have many dates, tournaments only one
+
+## Groups (SendouQ)
+
+```mermaid
+erDiagram
+ Group ||--o{ GroupMember : has
+ User ||--o{ GroupMember : member_of
+
+ Group ||--o{ GroupLike : likes
+ Group ||--o{ GroupLike : liked_by
+
+ Group ||--o| GroupMatch : alpha_in
+ Group ||--o| GroupMatch : bravo_in
+ User ||--o{ GroupMatch : reported_by
+
+ GroupMatch ||--|{ GroupMatchMap : has
+ Group ||--o| Team : team_id
+```
+
+### Notes
+
+- Even if a group rejoins the queue with the same players after the match, a new "Group" is created in the DB
+
+## LFG Posts
+
+```mermaid
+erDiagram
+ LFGPost }o--|| User : author
+ LFGPost }o--o| Team : team
+```
+
+## Map Pools
+
+```mermaid
+erDiagram
+ MapPoolMap }o--|| CalendarEvent : calendar_event
+ MapPoolMap }o--|| CalendarEvent : tie_breaker_calendar_event
+ MapPoolMap }o--|| TournamentTeam : tournament_team
+```
+
+### Notes
+
+Can be one of the following:
+1) Regular calendar events map pool
+2) Tournament's tiebreaker maps (teams' pick mode, AUTO_ALL)
+3) Tournament's map pool (TO's map picking mode)
+4) Tournament teams map picks (teams' pick mode, AUTO_ALL, AUTO_SZ etc.)
+
+## Plus Server Suggestions
+
+```mermaid
+erDiagram
+ PlusSuggestion }o--|| User : author
+ PlusSuggestion }o--|| User : suggested
+```
+
+### Notes
+
+- Comments to suggestions are also just suggestions same as new suggestions
+
+## Plus Server Tiers
+
+```mermaid
+erDiagram
+ PlusTier |o--|| User : userId
+```
+
+### Views
+- **FreshPlusTier** - Calculates Plus Server Tiers based on the latest voting results
+
+### Notes
+- PlusTier is just FreshPlusTier materialized for performance reasons with players from the leaderboard added
+
+## Results (maps/head-to-head)
+
+```mermaid
+erDiagram
+ User ||--o{ MapResult : has
+ PlayerResult }o--|| User : owner
+ PlayerResult }o--|| User : other
+```
+
+### Notes
+
+- Denormalized tables to make fetching these efficient
+
+## Scrims
+
+```mermaid
+erDiagram
+ ScrimPost ||--|{ ScrimPostUser : has
+ ScrimPost ||--o{ ScrimPostRequest : has
+ ScrimPostRequest ||--|{ ScrimPostRequestUser : has
+
+ User ||--o{ ScrimPostUser : participates
+ User ||--o{ ScrimPostRequestUser : participates
+```
+
+## Teams
+
+```mermaid
+erDiagram
+ AllTeam ||--o{ AllTeamMember : has
+ User ||--o{ AllTeamMember : member_of
+```
+
+### Views
+
+- **Team** - Teams excluding disbanded
+- **TeamMember** - `AllTeamMember` excluding members who already left their team & secondary teams
+- **TeamMemberWithSecondary** - `AllTeamMember` excluding members who already left their team but including secondary teams
+
+## Tournaments
+
+The database structure is mimicking the `brackets-manager.js` library. See this issue for a schema: [https://github.com/Drarig29/brackets-manager.js/issues/111#issuecomment-997417423](https://github.com/Drarig29/brackets-manager.js/issues/111#issuecomment-997417423)
+
+## Tournament organizations
+
+```mermaid
+erDiagram
+ TournamentOrganization ||--|{ TournamentOrganizationMember : has_member
+ User ||--o{ TournamentOrganizationMember : member_of
+
+ TournamentOrganization ||--o{ TournamentOrganizationBadge : has_badge
+ Badge ||--o{ TournamentOrganizationBadge : badge_of
+
+ TournamentOrganization ||--o{ TournamentOrganizationSeries : has_series
+```
+
+## Videos
+```mermaid
+erDiagram
+ UnvalidatedVideo ||--|{ VideoMatch : has
+ VideoMatch ||--o{ VideoMatchPlayer : has
+```
+
+### Notes
+
+- `Video` - Same as `UnvalidatedVideo` (redundant)
diff --git a/docs/dev/how-to.md b/docs/dev/how-to.md
new file mode 100644
index 000000000..de9e1702a
--- /dev/null
+++ b/docs/dev/how-to.md
@@ -0,0 +1,54 @@
+# How to...
+
+Guides on how to do different things when developing sendou.ink
+
+## Fix style/lint errors (Biome)
+
+Run the `npm run biome:fix` command. Also you might want to set up Biome as an extension to your IDE and run automatically when you save a file.
+
+## Add a new database migration
+
+1) Add a new file to the migrations folder incrementing the last used number by one e.g. `011-my-cool-feature.js`. Note: there is no script to do this.
+2) Take this file as a base and fill it out with your migration:
+
+```js
+export function up(db) {
+ db.transaction(() => {
+ // your migrations go here
+ })();
+}
+```
+
+Note: No need to implement the "down" migration
+
+3) Update the typings in `app/db/tables.ts`
+4) Run `npm run migrate up` to apply your migration
+4) Set env var `DB_PATH=db-test.sqlite3` in `.env` file & run the `npm run migrate up` command again to update the database used in unit tests
+
+## Add a new translation string
+
+1) Decide on where the translation should go. Either `common.json` which is available in every route by default or a feature specific one such as `builds.json`
+2) Add the translation string to the json with some descriptive key
+3) Access in code via the `useTranslation` hook
+
+```json
+// common.json
+{
+ ...
+ "my-cool.translation": "Translated"
+ ...
+}
+```
+
+```tsx
+// CoolComponent.tsx
+export function CoolComponent() {
+ const { t } = useTranslation(["common"]);
+
+ return (
+ {t("common:my-cool.translation")}
+ )
+}
+```
+
+When utilizing feature specific translations ensure the json is loaded. This is handled via the `handle` Remix function.
diff --git a/docs/dev/scripts.md b/docs/dev/scripts.md
new file mode 100644
index 000000000..93c5f9d5d
--- /dev/null
+++ b/docs/dev/scripts.md
@@ -0,0 +1,75 @@
+# Scripts
+
+Note: These are mostly useful if you are running the site in production as an admin, not typically for development.
+
+---
+
+## Add new badge to the database
+
+```bash
+npx tsx scripts/add-badge.ts fire_green "Octofin Eliteboard"
+```
+
+## Rename display name of a badge
+
+```bash
+npx tsx scripts/rename-badge.ts 10 "New 4v4 Sundaes"
+```
+
+## Add many badge owners
+
+```bash
+npx tsx scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
+```
+
+## Converting gifs (badges) to thumbnail (.png)
+
+```bash
+sips -s format png ./sundae.gif --out .
+```
+
+## Convert many .png files to .avif
+
+While in the folder with the images:
+
+```bash
+for i in *.png; do npx @squoosh/cli --avif '{"cqLevel":33,"cqAlphaLevel":-1,"denoiseLevel":0,"tileColsLog2":0,"tileRowsLog2":0,"speed":6,"subsample":1,"chromaDeltaQ":false,"sharpness":0,"tune":0}' $i; done
+```
+
+Note: it only works with Node 16.
+
+## Doing monthly update
+
+1. Fill /scripts/dicts with new data from leanny repository:
+ - weapon = contents of `weapon` folder
+ - langs = contents of `language` folder
+ - Couple of others at the root: `GearInfoClothes.json`, `GearInfoHead.json`, `GearInfoShoes.json`, `spl__DamageRateInfoConfig.pp__CombinationDataTableData.json`, `SplPlayer.game__GameParameterTable.json`, `WeaponInfoMain.json`, `WeaponInfoSpecial.json` and `WeaponInfoSub.json`
+1. Update all `CURRENT_SEASON` constants
+1. Update `CURRENT_PATCH` constants
+1. Update `PATCHES` constant with the late patch + remove the oldest
+1. Update the stage list in `stage-ids.ts` and `create-misc-json.ts`. Add images from Lean's repository and avify them.
+1. `npx tsx scripts/create-misc-json.ts`
+1. `npx tsx scripts/create-gear-json.ts`
+1. `npx tsx scripts/create-analyzer-json.ts`
+ 8a. Double check that no hard-coded special damages changed
+1. `npx tsx scripts/create-object-dmg-json.ts`
+1. Fill new weapon IDs by category to `weapon-ids.ts` (easy to take from the diff of English weapons.json)
+1. Get gear IDs for each slot from /output folder and update `gear-ids.ts`.
+1. Replace `object-dmg.json` with the `object-dmg.json` in /output folder
+1. Replace `weapon-params.ts` with the `params.json` in /output folder
+1. Delete all images inside `main-weapons`, `main-weapons-outlined`, `main-weapons-outlined-2` and `gear` folders.
+1. Replace with images from Lean's repository.
+1. Run the `npx tsx scripts/replace-img-names.ts` command
+1. Run the `npx tsx scripts/replace-weapon-names.ts` command
+1. Run the .avif generating command in each image folder.
+2. Update manually any languages that use English `gear.json` and `weapons.json` files
+
+## Download the production database from Render.com
+
+Note: This is only useful if you have access to a production running on Render.com
+
+1. Access the "Shell" tab
+2. `cd /var/data`
+3. `cp db.sqlite3 db-copy.sqlite3`
+4. `wormhole send db-copy.sqlite3`
+5. On the receiving computer use the command shown.
diff --git a/docs/translation.md b/docs/translation.md
new file mode 100644
index 000000000..bcd5d43e5
--- /dev/null
+++ b/docs/translation.md
@@ -0,0 +1,20 @@
+# Translation
+
+[Translation Progress](https://github.com/Sendouc/sendou.ink/issues/1104)
+
+sendou.ink can be translated into any language. All the translations can be found in the [locales folder](./locales). Here is how you can contribute:
+
+1. Copy a `.json` file from `/en` folder.
+2. Translate lines one by one. For example `"country": "Country",` could become `"country": "Maa",`. Keep the "key" on the left side of : unchanged.
+3. Finally, send the translated .json to Sendou or make a pull request if you know how.
+
+Things to note:
+
+- `weapons.json` and `gear.json` are auto-generated. Don't touch these.
+- If some language doesn't have a folder it can be added.
+- Some translated `.json` files can also have some lines in English as new lines get added to the site. Those can then be translated.
+- Some lines have a dynamic part like this one: `"articleBy": "by {{author}}"` in this case `{{author}}` should appear in the translated version unchanged. So in other words don't translate the part inside `{{}}`.
+- There is one more special syntax to keep in mind. When you translate this line `"project": "Sendou.ink is a project by <2>Sendou2> with help from contributors:",` the `<2>2>` should appear in the translated version. The text inside these tags can change.
+- To update a translation file copy the existing file, do any modifications needed and send the updated one.
+
+Any questions please ask Sendou!
diff --git a/package-lock.json b/package-lock.json
index 5222c5a99..d5510282f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -80,6 +80,7 @@
"cross-env": "^7.0.3",
"ley": "^0.8.1",
"sql-formatter": "^15.6.1",
+ "tsx": "^4.19.4",
"typescript": "^5.8.3",
"vite": "^6.3.4",
"vite-plugin-babel": "^1.3.0",
@@ -10442,6 +10443,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
+ "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -15085,6 +15099,16 @@
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -16423,6 +16447,492 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.19.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
+ "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+ "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
+ "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
+ "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
+ "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
+ "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
+ "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
+ "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
+ "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
+ "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
+ "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
+ "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
+ "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
+ "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
+ "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
+ "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
+ "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
+ "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
+ "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
+ "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
+ "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
+ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.4",
+ "@esbuild/android-arm": "0.25.4",
+ "@esbuild/android-arm64": "0.25.4",
+ "@esbuild/android-x64": "0.25.4",
+ "@esbuild/darwin-arm64": "0.25.4",
+ "@esbuild/darwin-x64": "0.25.4",
+ "@esbuild/freebsd-arm64": "0.25.4",
+ "@esbuild/freebsd-x64": "0.25.4",
+ "@esbuild/linux-arm": "0.25.4",
+ "@esbuild/linux-arm64": "0.25.4",
+ "@esbuild/linux-ia32": "0.25.4",
+ "@esbuild/linux-loong64": "0.25.4",
+ "@esbuild/linux-mips64el": "0.25.4",
+ "@esbuild/linux-ppc64": "0.25.4",
+ "@esbuild/linux-riscv64": "0.25.4",
+ "@esbuild/linux-s390x": "0.25.4",
+ "@esbuild/linux-x64": "0.25.4",
+ "@esbuild/netbsd-arm64": "0.25.4",
+ "@esbuild/netbsd-x64": "0.25.4",
+ "@esbuild/openbsd-arm64": "0.25.4",
+ "@esbuild/openbsd-x64": "0.25.4",
+ "@esbuild/sunos-x64": "0.25.4",
+ "@esbuild/win32-arm64": "0.25.4",
+ "@esbuild/win32-ia32": "0.25.4",
+ "@esbuild/win32-x64": "0.25.4"
+ }
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
diff --git a/package.json b/package.json
index c6ae05702..7fbe09459 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,8 @@
"scripts": {
"deploy": "npm ci && npm run build",
"build": "remix vite:build",
- "dev": "remix vite:dev --host",
+ "dev": "cross-env DB_PATH=db.sqlite3 npm run migrate up && npm run setup && remix vite:dev --host",
"dev:prod": "DB_PATH=db-prod.sqlite3 VITE_PROD_MODE=true npm run dev",
- "dev:ci": "cp .env.example .env && npm run migrate up && npm run dev",
"start": "npm run migrate up && remix-serve ./build/server/index.js",
"migrate": "ley",
"check-translation-jsons": "node --experimental-strip-types scripts/check-translation-jsons.ts",
@@ -23,7 +22,8 @@
"test:unit": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only run",
"test:e2e": "npx playwright test",
"test:e2e:flaky-detect": "npx playwright test --repeat-each=10 --max-failures=1",
- "checks": "npm run biome:fix && npm run test:unit && npm run check-translation-jsons && npm run typecheck"
+ "checks": "npm run biome:fix && npm run test:unit && npm run check-translation-jsons && npm run typecheck",
+ "setup": "cross-env DB_PATH=db.sqlite3 tsx ./scripts/setup.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.797.0",
@@ -98,6 +98,7 @@
"cross-env": "^7.0.3",
"ley": "^0.8.1",
"sql-formatter": "^15.6.1",
+ "tsx": "^4.19.4",
"typescript": "^5.8.3",
"vite": "^6.3.4",
"vite-plugin-babel": "^1.3.0",
diff --git a/playwright.config.ts b/playwright.config.ts
index d6e2d9421..1871f47a2 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -99,7 +99,7 @@ const config: PlaywrightTestConfig = {
/* Run your local dev server before starting the tests */
webServer: {
- command: "npm run dev:ci",
+ command: "npm run dev",
port: 5173,
reuseExistingServer: !process.env.CI,
},
diff --git a/scripts/setup.ts b/scripts/setup.ts
new file mode 100644
index 000000000..26b93611a
--- /dev/null
+++ b/scripts/setup.ts
@@ -0,0 +1,33 @@
+import "dotenv/config";
+import fs from "node:fs";
+import { seed } from "~/db/seed";
+import { db } from "~/db/sql";
+import { logger } from "~/utils/logger";
+
+async function main() {
+ // Step 1: Create .env if it doesn't exist
+ if (!fs.existsSync(".env")) {
+ logger.info("📄 .env not found. Creating from .env.example...");
+ fs.copyFileSync(".env.example", ".env");
+ logger.info(".env created with defaults values");
+ }
+
+ const dbEmpty = !(await db.selectFrom("User").selectAll().executeTakeFirst());
+
+ // Step 2: Run migration and seed if db.sqlite3 doesn't exist
+ if (dbEmpty) {
+ logger.info("🌱 Seeding database...");
+ try {
+ await seed();
+ logger.info("Database seeded successfully");
+ } catch (err) {
+ logger.error(
+ "Error running migrate or seed scripts:",
+ (err as Error).message,
+ );
+ process.exit(1);
+ }
+ }
+}
+
+main();