feat(lint): add biome formatter and linter, husky and lint-staged

This commit adds the Biome formatter and linter to replace Prettier, including:

- Add biome.json config file
- Add pre-commit hook with Husky
- Configure GitHub Action to run Biome checks
- Apply Biome formatting rules to codebase
- Remove Prettier dependencies
This commit is contained in:
Shintaro Jokagi 2025-05-15 13:52:37 +12:00
parent b4e5fe2bea
commit bcb1427a79
No known key found for this signature in database
GPG key ID: 0DDF8FA44C9A0DA8
50 changed files with 904 additions and 793 deletions

View file

@ -21,5 +21,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm install --no-frozen-lockfile run: npm install --no-frozen-lockfile
- name: Run Biome check
run: npx biome check ./src
- name: Build project - name: Build project
run: npm run build run: npm run build

4
.husky/pre-commit Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

25
biome.json Normal file
View file

@ -0,0 +1,25 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"files": {
"ignore": [
"node_modules",
".git",
"dist"
]
}
}

275
package-lock.json generated
View file

@ -28,14 +28,13 @@
"motion": "^11.13.5", "motion": "^11.13.5",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"preact": "^10.26.2", "preact": "^10.26.2",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"typescript": "^5.6.3" "typescript": "^5.6.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4",
"husky": "^9.1.7",
"wrangler": "^3.94.0" "wrangler": "^3.94.0"
} }
}, },
@ -843,6 +842,170 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@biomejs/biome": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4",
"@biomejs/cli-darwin-x64": "1.9.4",
"@biomejs/cli-linux-arm64": "1.9.4",
"@biomejs/cli-linux-arm64-musl": "1.9.4",
"@biomejs/cli-linux-x64": "1.9.4",
"@biomejs/cli-linux-x64-musl": "1.9.4",
"@biomejs/cli-win32-arm64": "1.9.4",
"@biomejs/cli-win32-x64": "1.9.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@capsizecss/unpack": { "node_modules/@capsizecss/unpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-2.4.0.tgz",
@ -4959,6 +5122,22 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/import-meta-resolve": { "node_modules/import-meta-resolve": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@ -6830,6 +7009,8 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@ -6845,6 +7026,8 @@
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz",
"integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.9.1", "@astrojs/compiler": "^2.9.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
@ -6854,84 +7037,6 @@
"node": "^14.15.0 || >=16.0.0" "node": "^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.11",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
"integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
"license": "MIT",
"engines": {
"node": ">=14.21.3"
},
"peerDependencies": {
"@ianvs/prettier-plugin-sort-imports": "*",
"@prettier/plugin-pug": "*",
"@shopify/prettier-plugin-liquid": "*",
"@trivago/prettier-plugin-sort-imports": "*",
"@zackad/prettier-plugin-twig": "*",
"prettier": "^3.0",
"prettier-plugin-astro": "*",
"prettier-plugin-css-order": "*",
"prettier-plugin-import-sort": "*",
"prettier-plugin-jsdoc": "*",
"prettier-plugin-marko": "*",
"prettier-plugin-multiline-arrays": "*",
"prettier-plugin-organize-attributes": "*",
"prettier-plugin-organize-imports": "*",
"prettier-plugin-sort-imports": "*",
"prettier-plugin-style-order": "*",
"prettier-plugin-svelte": "*"
},
"peerDependenciesMeta": {
"@ianvs/prettier-plugin-sort-imports": {
"optional": true
},
"@prettier/plugin-pug": {
"optional": true
},
"@shopify/prettier-plugin-liquid": {
"optional": true
},
"@trivago/prettier-plugin-sort-imports": {
"optional": true
},
"@zackad/prettier-plugin-twig": {
"optional": true
},
"prettier-plugin-astro": {
"optional": true
},
"prettier-plugin-css-order": {
"optional": true
},
"prettier-plugin-import-sort": {
"optional": true
},
"prettier-plugin-jsdoc": {
"optional": true
},
"prettier-plugin-marko": {
"optional": true
},
"prettier-plugin-multiline-arrays": {
"optional": true
},
"prettier-plugin-organize-attributes": {
"optional": true
},
"prettier-plugin-organize-imports": {
"optional": true
},
"prettier-plugin-sort-imports": {
"optional": true
},
"prettier-plugin-style-order": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
}
}
},
"node_modules/printable-characters": { "node_modules/printable-characters": {
"version": "1.0.42", "version": "1.0.42",
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
@ -7448,13 +7553,17 @@
"version": "0.0.15", "version": "0.0.15",
"resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz",
"integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==",
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/sass-formatter": { "node_modules/sass-formatter": {
"version": "0.7.9", "version": "0.7.9",
"resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz",
"integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"suf-log": "^2.5.3" "suf-log": "^2.5.3"
} }
@ -7855,6 +7964,8 @@
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
"integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"s.color": "0.0.15" "s.color": "0.0.15"
} }

View file

@ -8,7 +8,10 @@
"build": "astro check && astro build", "build": "astro check && astro build",
"preview": "astro preview --port 3000", "preview": "astro preview --port 3000",
"wrangler": "wrangler", "wrangler": "wrangler",
"astro": "astro" "astro": "astro",
"lint": "biome lint ./src",
"format": "biome format ./src --write",
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
@ -31,14 +34,19 @@
"motion": "^11.13.5", "motion": "^11.13.5",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"preact": "^10.26.2", "preact": "^10.26.2",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"typescript": "^5.6.3" "typescript": "^5.6.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4",
"husky": "^9.1.7",
"lint-staged": "^15.2.7",
"wrangler": "^3.94.0" "wrangler": "^3.94.0"
},
"lint-staged": {
"src/**/*.{ts,tsx,astro,js,jsx}": [
"biome lint --apply"
]
} }
} }

View file

@ -1,19 +1,19 @@
export function getTitleAnimation(delay = 0, duration = 0.3, once = true) { export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
return { return {
initial: { opacity: 0.001, translateY: 20, filter: 'blur(4px)' }, initial: { opacity: 0.001, translateY: 20, filter: "blur(4px)" },
whileInView: { whileInView: {
opacity: 1, opacity: 1,
translateY: 0, translateY: 0,
filter: 'blur(0px)', filter: "blur(0px)",
transition: { duration, delay }, transition: { duration, delay },
}, },
viewport: { once: once }, viewport: { once: once },
} };
} }
export function getZoomInAnimation(delay = 0) { export function getZoomInAnimation(delay = 0) {
return { return {
initial: { scale: 0.8, opacity: 0.001 }, initial: { scale: 0.8, opacity: 0.001 },
whileInView: { scale: 1, opacity: 1, transition: { duration: 0.2, delay } }, whileInView: { scale: 1, opacity: 1, transition: { duration: 0.2, delay } },
} };
} }

View file

@ -1,14 +1,14 @@
--- ---
import { ArrowLeft } from 'lucide-astro' import { ArrowLeft } from "lucide-astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { routes: {
mods: { slug }, mods: { slug },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<button <button

View file

@ -1,17 +1,9 @@
--- ---
import { getLocale, getPath } from '~/utils/i18n' import { getLocale, getPath } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = Astro.props;
class: className,
isPrimary,
isAlert,
isBordered,
href,
id,
extra,
} = Astro.props
--- ---
{ {

View file

@ -1,7 +1,7 @@
--- ---
const { white, multiplier = 0.9, class: classList } = Astro.props const { white, multiplier = 0.9, class: classList } = Astro.props;
const sizes = [216, 396, 576, 756] const sizes = [216, 396, 576, 756];
const borderWidths = [20, 30, 40, 50] const borderWidths = [20, 30, 40, 50];
--- ---
<div <div

View file

@ -1,20 +1,20 @@
--- ---
import Image from 'astro/components/Image.astro' import Image from "astro/components/Image.astro";
import { Check, Github } from 'lucide-astro' import { Check, Github } from "lucide-astro";
import { motion } from 'motion/react' import { motion } from "motion/react";
import { getTitleAnimation } from '~/animations' import { getTitleAnimation } from "~/animations";
import ComImage from '~/assets/ComImage.png' import ComImage from "~/assets/ComImage.png";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { routes: {
index: { community }, index: { community },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<section <section

View file

@ -1,33 +1,31 @@
--- ---
import { motion } from 'motion/react' import { motion } from "motion/react";
import { getTitleAnimation } from '~/animations' import { getTitleAnimation } from "~/animations";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import CompactModeVideo from '~/assets/CompactMode.webm' import CompactModeVideo from "~/assets/CompactMode.webm";
import GlanceVideo from '~/assets/Glance.webm' import GlanceVideo from "~/assets/Glance.webm";
import SplitViewsVideo from '~/assets/SplitViews.webm' import SplitViewsVideo from "~/assets/SplitViews.webm";
import WorkspacesVideo from '~/assets/Workspaces.webm' import WorkspacesVideo from "~/assets/Workspaces.webm";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
import Video from './Video.astro' import Video from "./Video.astro";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { routes: {
index: { features }, index: { features },
}, },
} = getUI(locale) } = getUI(locale);
const { const {
title1 = features.title1, title1 = features.title1,
title2 = features.title2, title2 = features.title2,
title3 = features.title3, title3 = features.title3,
} = Astro.props } = Astro.props;
const descriptions = Object.values(features.featureTabs).map( const descriptions = Object.values(features.featureTabs).map((tab) => tab.description);
(tab) => tab.description,
)
--- ---
<section <section

View file

@ -1,16 +1,16 @@
--- ---
import { ArrowRight } from 'lucide-astro' import { ArrowRight } from "lucide-astro";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Circles from '~/components/Circles.astro' import Circles from "~/components/Circles.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import SocialMediaStrip from '~/components/SocialMediaStrip.astro' import SocialMediaStrip from "~/components/SocialMediaStrip.astro";
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
components: { footer }, components: { footer },
} = getUI(locale) } = getUI(locale);
--- ---
<footer <footer

View file

@ -1,34 +1,34 @@
--- ---
import { ArrowRight } from 'lucide-astro' import { ArrowRight } from "lucide-astro";
import { motion } from 'motion/react' import { motion } from "motion/react";
import { getTitleAnimation } from '~/animations' import { getTitleAnimation } from "~/animations";
import HomePageVideo from '~/assets/HomePageVideo.webm' import HomePageVideo from "~/assets/HomePageVideo.webm";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import Title from '~/components/Title.astro' import Title from "~/components/Title.astro";
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
import SocialMediaStrip from './SocialMediaStrip.astro' import SocialMediaStrip from "./SocialMediaStrip.astro";
import Video from './Video.astro' import Video from "./Video.astro";
let titleAnimationCounter = 0 let titleAnimationCounter = 0;
function getNewAnimationDelay() { function getNewAnimationDelay() {
titleAnimationCounter++ titleAnimationCounter++;
return titleAnimationCounter * 0.15 return titleAnimationCounter * 0.15;
} }
function getHeroTitleAnimation() { function getHeroTitleAnimation() {
return getTitleAnimation(getNewAnimationDelay()) return getTitleAnimation(getNewAnimationDelay());
} }
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
routes: { routes: {
index: { hero }, index: { hero },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<header <header

View file

@ -1,5 +1,5 @@
--- ---
const { class: className } = Astro.props const { class: className } = Astro.props;
--- ---
<svg <svg

View file

@ -1,13 +1,13 @@
--- ---
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
components: { components: {
nav: { menu }, nav: { menu },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<!-- Hidden checkbox for menu toggle --> <!-- Hidden checkbox for menu toggle -->

View file

@ -1,21 +1,21 @@
import { icon, library } from '@fortawesome/fontawesome-svg-core' import { icon, library } from "@fortawesome/fontawesome-svg-core";
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons' import { faSort, faSortDown, faSortUp } from "@fortawesome/free-solid-svg-icons";
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from "preact/hooks";
import { useModsSearch } from '~/hooks/useModsSearch' import { useModsSearch } from "~/hooks/useModsSearch";
import type { ZenTheme } from '~/mods' import type { ZenTheme } from "~/mods";
import { type Locale, getUI } from '~/utils/i18n' import { type Locale, getUI } from "~/utils/i18n";
// Add icons to the library // Add icons to the library
library.add(faSort, faSortUp, faSortDown) library.add(faSort, faSortUp, faSortDown);
// Create icon objects // Create icon objects
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' }) const defaultSortIcon = icon({ prefix: "fas", iconName: "sort" });
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' }) const ascSortIcon = icon({ prefix: "fas", iconName: "sort-up" });
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' }) const descSortIcon = icon({ prefix: "fas", iconName: "sort-down" });
interface ModsListProps { interface ModsListProps {
allMods: ZenTheme[] allMods: ZenTheme[];
locale: Locale locale: Locale;
} }
export default function ModsList({ allMods, locale }: ModsListProps) { export default function ModsList({ allMods, locale }: ModsListProps) {
@ -34,73 +34,71 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
setLimit, setLimit,
mods: paginatedMods, mods: paginatedMods,
// searchParams, // searchParams,
} = useModsSearch(allMods) } = useModsSearch(allMods);
const [pageInput, setPageInput] = useState(page.toString()) const [pageInput, setPageInput] = useState(page.toString());
// Keep page input in sync with actual page // Keep page input in sync with actual page
useEffect(() => { useEffect(() => {
setPageInput(page.toString()) setPageInput(page.toString());
}, [page]) }, [page]);
function getSortIcon(state: 'default' | 'asc' | 'desc') { function getSortIcon(state: "default" | "asc" | "desc") {
if (state === 'asc') return ascSortIcon if (state === "asc") return ascSortIcon;
if (state === 'desc') return descSortIcon if (state === "desc") return descSortIcon;
return defaultSortIcon return defaultSortIcon;
} }
function handleSearch(e: Event) { function handleSearch(e: Event) {
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement;
setSearch(target.value) setSearch(target.value);
} }
function handleLimitChange(e: Event) { function handleLimitChange(e: Event) {
const target = e.target as HTMLSelectElement const target = e.target as HTMLSelectElement;
setLimit(Number.parseInt(target.value, 10)) setLimit(Number.parseInt(target.value, 10));
} }
function handlePageSubmit(e: Event) { function handlePageSubmit(e: Event) {
e.preventDefault() e.preventDefault();
const newPage = Number.parseInt(pageInput, 10) const newPage = Number.parseInt(pageInput, 10);
if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) { if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) {
setPage(newPage) setPage(newPage);
window.scrollTo(0, 0) window.scrollTo(0, 0);
} else { } else {
setPageInput(page.toString()) setPageInput(page.toString());
} }
} }
function handlePageInputChange(e: Event) { function handlePageInputChange(e: Event) {
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement;
setPageInput(target.value) setPageInput(target.value);
} }
function navigatePage(pageNum: number) { function navigatePage(pageNum: number) {
setPage(pageNum) setPage(pageNum);
window.scrollTo(0, 0) window.scrollTo(0, 0);
} }
const { const {
routes: { mods }, routes: { mods },
} = getUI(locale) } = getUI(locale);
function renderPagination() { function renderPagination() {
if (totalPages <= 1) return null if (totalPages <= 1) return null;
return ( return (
<div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8"> <div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8">
<button <button
type="button" type="button"
onClick={() => navigatePage(page - 1)} onClick={() => navigatePage(page - 1)}
className={`px-3 py-2 ${ className={`px-3 py-2 ${
page === 1 page === 1 ? "pointer-events-none text-gray-400" : "text-dark hover:text-gray-600"
? 'pointer-events-none text-gray-400'
: 'text-dark hover:text-gray-600'
}`} }`}
> >
&lt; &lt;
</button> </button>
<form onSubmit={handlePageSubmit} className="flex items-center gap-2"> <form onSubmit={handlePageSubmit} className="flex items-center gap-2">
{mods.pagination.pagination.split('{input}').map((value, index) => { {mods.pagination.pagination.split("{input}").map((value, index) => {
if (index === 0) { if (index === 0) {
return ( return (
<input <input
@ -110,15 +108,15 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm" className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
aria-label="Page number" aria-label="Page number"
/> />
) );
} }
return ( return (
<span className="text-sm"> <span key={value} className="text-sm">
{value {value
.replace('{totalPages}', totalPages.toString()) .replace("{totalPages}", totalPages.toString())
.replace('{totalItems}', totalItems.toString())} .replace("{totalItems}", totalItems.toString())}
</span> </span>
) );
})} })}
</form> </form>
<button <button
@ -126,14 +124,14 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
onClick={() => navigatePage(page + 1)} onClick={() => navigatePage(page + 1)}
className={`px-3 py-2 ${ className={`px-3 py-2 ${
page === totalPages page === totalPages
? 'pointer-events-none text-gray-400' ? "pointer-events-none text-gray-400"
: 'text-dark hover:text-gray-600' : "text-dark hover:text-gray-600"
}`} }`}
> >
&gt; &gt;
</button> </button>
</div> </div>
) );
} }
return ( return (
@ -159,6 +157,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
> >
{mods.sort.lastCreated} {mods.sort.lastCreated}
<span <span
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: getSortIcon(createdSort).html[0], __html: getSortIcon(createdSort).html[0],
}} }}
@ -174,6 +173,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
> >
{mods.sort.lastUpdated} {mods.sort.lastUpdated}
<span <span
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: getSortIcon(updatedSort).html[0], __html: getSortIcon(updatedSort).html[0],
}} }}
@ -218,10 +218,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
</div> </div>
<div> <div>
<h2 className="text-lg font-bold"> <h2 className="text-lg font-bold">
{mod.name}{' '} {mod.name} <span className="ml-1 text-sm font-normal">by @{mod.author}</span>
<span className="ml-1 text-sm font-normal">
by @{mod.author}
</span>
</h2> </h2>
<p className="text-sm font-thin">{mod.description}</p> <p className="text-sm font-thin">{mod.description}</p>
</div> </div>
@ -237,5 +234,5 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
{renderPagination()} {renderPagination()}
</div> </div>
) );
} }

View file

@ -1,21 +1,21 @@
--- ---
import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar' import { Astronav, Dropdown, DropdownItems, MenuItems } from "astro-navbar";
import { ArrowRight, ChevronDown, Download, Menu } from 'lucide-astro' import { ArrowRight, ChevronDown, Download, Menu } from "lucide-astro";
import { motion } from 'motion/react' import { motion } from "motion/react";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
import { getTitleAnimation } from '../animations.ts' import { getTitleAnimation } from "../animations.ts";
import Logo from './Logo.astro' import Logo from "./Logo.astro";
import MobileMenu from './MobileMenu.astro' import MobileMenu from "./MobileMenu.astro";
import ThemeSwitch from './ThemeSwitch.astro' import ThemeSwitch from "./ThemeSwitch.astro";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
components: { components: {
nav: { brand, menu }, nav: { brand, menu },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<!-- Desktop Navigation --> <!-- Desktop Navigation -->

View file

@ -1,41 +1,41 @@
--- ---
import { Accordion, AccordionItem } from 'free-astro-components' import { Accordion, AccordionItem } from "free-astro-components";
import { Info } from 'lucide-astro' import { Info } from "lucide-astro";
import { releaseNotes as releaseNotesData } from '~/release-notes' import { releaseNotes as releaseNotesData } from "~/release-notes";
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
import { import {
type BreakingChange, type BreakingChange,
type ReleaseNote, type ReleaseNote,
getReleaseNoteFirefoxVersion, getReleaseNoteFirefoxVersion,
} from '../release-notes' } from "../release-notes";
export type Props = ReleaseNote export type Props = ReleaseNote;
const { isTwilight, ...props } = Astro.props const { isTwilight, ...props } = Astro.props;
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
routes: { routes: {
releaseNotes: { releaseNotes: {
components: { releaseNoteItem }, components: { releaseNoteItem },
}, },
}, },
} = getUI(locale) } = getUI(locale);
let date let date: Date | undefined;
if (props.date) { if (props.date) {
const [day, month, year] = props.date.split('/') const [day, month, year] = props.date.split("/");
date = new Date(Date.parse(`${year}-${month}-${day}`)) date = new Date(Date.parse(`${year}-${month}-${day}`));
} }
const ffVersion = getReleaseNoteFirefoxVersion(props) const ffVersion = getReleaseNoteFirefoxVersion(props);
const currentReleaseIndex = releaseNotesData.findIndex( const currentReleaseIndex = releaseNotesData.findIndex(
(releaseNote: ReleaseNote) => releaseNote.version === props.version, (releaseNote: ReleaseNote) => releaseNote.version === props.version,
) );
const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1] const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1];
let compareLink = '' let compareLink = "";
if (prevReleaseNote && !isTwilight) { if (prevReleaseNote && !isTwilight) {
compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}` compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}`;
} }
--- ---

View file

@ -1,21 +1,21 @@
--- ---
const { gap = 4 } = Astro.props const { gap = 4 } = Astro.props;
import { icon, library } from '@fortawesome/fontawesome-svg-core' import { icon, library } from "@fortawesome/fontawesome-svg-core";
import { import {
faBluesky, faBluesky,
faGithub, faGithub,
faMastodon, faMastodon,
faReddit, faReddit,
faXTwitter, faXTwitter,
} from '@fortawesome/free-brands-svg-icons' } from "@fortawesome/free-brands-svg-icons";
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit) library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit);
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' }) const Mastodon = icon({ prefix: "fab", iconName: "mastodon" });
const Bluesky = icon({ prefix: 'fab', iconName: 'bluesky' }) const Bluesky = icon({ prefix: "fab", iconName: "bluesky" });
const Github = icon({ prefix: 'fab', iconName: 'github' }) const Github = icon({ prefix: "fab", iconName: "github" });
const XTwitter = icon({ prefix: 'fab', iconName: 'x-twitter' }) const XTwitter = icon({ prefix: "fab", iconName: "x-twitter" });
const Reddit = icon({ prefix: 'fab', iconName: 'reddit' }) const Reddit = icon({ prefix: "fab", iconName: "reddit" });
--- ---
<ul class={`flex items-center opacity-80 gap-${gap}`}> <ul class={`flex items-center opacity-80 gap-${gap}`}>

View file

@ -1,21 +1,21 @@
--- ---
import { motion } from 'motion/react' import { motion } from "motion/react";
import { getTitleAnimation } from '~/animations' import { getTitleAnimation } from "~/animations";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
import tutaLogo from '~/assets/tuta-logo.png' import tutaLogo from "~/assets/tuta-logo.png";
import Image from 'astro/components/Image.astro' import Image from "astro/components/Image.astro";
const { showSponsors = true } = Astro.props const { showSponsors = true } = Astro.props;
const { const {
routes: { routes: {
index: { sponsors }, index: { sponsors },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<section id="sponsors" class:list={['mb-32 px-4', !showSponsors && 'hidden']}> <section id="sponsors" class:list={['mb-32 px-4', !showSponsors && 'hidden']}>

View file

@ -1,10 +1,10 @@
--- ---
interface Props { interface Props {
label?: string label?: string;
className?: string className?: string;
} }
const { label, className = '' } = Astro.props const { label, className = "" } = Astro.props;
--- ---
<button <button

View file

@ -1,5 +1,5 @@
--- ---
const { class: className } = Astro.props const { class: className } = Astro.props;
--- ---
<h1 class:list={['title text-dark', className]}> <h1 class:list={['title text-dark', className]}>

View file

@ -1,6 +1,6 @@
--- ---
const { src, class: className, ...rest } = Astro.props const { src, class: className, ...rest } = Astro.props;
const type = src.split('.').pop() || 'webm' const type = src.split(".").pop() || "webm";
--- ---
<video <video

View file

@ -1,12 +1,12 @@
--- ---
interface Props { interface Props {
label: string label: string;
href: string href: string;
variant?: string variant?: string;
checksum?: string checksum?: string;
} }
const { label, href, checksum } = Astro.props const { label, href, checksum } = Astro.props;
--- ---
<div class="relative flex flex-col"> <div class="relative flex flex-col">

View file

@ -1,17 +1,32 @@
--- ---
interface Props { interface ReleaseInfo {
platform: 'mac' | 'windows' | 'linux' label?: string;
icon: string[] link: string;
title: string checksum?: string;
description: string
releases: Record<string, any>
} }
const { platform, icon, title, description, releases } = Astro.props interface PlatformReleases {
import { Image } from 'astro:assets' universal?: ReleaseInfo;
import AppIconDark from '../../assets/app-icon-dark.png' all?: ReleaseInfo;
import AppIconLight from '../../assets/app-icon-light.png' tarball?: ReleaseInfo;
import DownloadCard from './ButtonCard.astro' x86_64?: { tarball: ReleaseInfo } | ReleaseInfo;
arm64?: ReleaseInfo;
flathub?: { all: ReleaseInfo };
}
interface Props {
platform: "mac" | "windows" | "linux";
icon: string[];
title: string;
description: string;
releases: PlatformReleases;
}
const { platform, icon, title, description, releases } = Astro.props;
import { Image } from "astro:assets";
import AppIconDark from "../../assets/app-icon-dark.png";
import AppIconLight from "../../assets/app-icon-light.png";
import DownloadCard from "./ButtonCard.astro";
--- ---
<div <div
@ -32,7 +47,7 @@ import DownloadCard from './ButtonCard.astro'
{ {
platform === 'linux' ? ( platform === 'linux' ? (
<> <>
<div> {releases.flathub && releases.flathub.all.label && <div>
<h4 class="mb-3 text-lg font-medium">Package Managers</h4> <h4 class="mb-3 text-lg font-medium">Package Managers</h4>
<div class="space-y-3"> <div class="space-y-3">
<DownloadCard <DownloadCard
@ -41,8 +56,8 @@ import DownloadCard from './ButtonCard.astro'
variant="flathub" variant="flathub"
/> />
</div> </div>
</div> </div>}
<div> {releases.x86_64 && 'tarball' in releases.x86_64 && <div>
<h4 class="mb-3 text-lg font-medium">Tarball</h4> <h4 class="mb-3 text-lg font-medium">Tarball</h4>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DownloadCard <DownloadCard
@ -53,23 +68,38 @@ import DownloadCard from './ButtonCard.astro'
/> />
<DownloadCard <DownloadCard
label="ARM64" label="ARM64"
href={releases.aarch64.tarball.link} href={releases.x86_64.tarball.link}
variant="aarch64" variant="aarch64"
checksum={releases.aarch64.tarball.checksum} checksum={releases.x86_64.tarball.checksum}
/> />
</div> </div>
</div> </div>}
</> </>
) : ( ) : (
<div class="space-y-4"> <div class="space-y-4">
{Object.entries(releases).map(([variant, releaseNote]) => ( <div class="space-y-3">
<DownloadCard {releases.universal && releases.universal.label && (
label={releaseNote.label} <DownloadCard
href={releaseNote.link} label={releases.universal.label}
variant={variant} href={releases.universal.link}
checksum={releaseNote.checksum} checksum={releases.universal.checksum}
/> />
))} )}
{releases.x86_64 && 'tarball' in releases.x86_64 && releases.x86_64.tarball && releases.x86_64.tarball.label && (
<DownloadCard
label={releases.x86_64.tarball.label}
href={releases.x86_64.tarball.link}
checksum={releases.x86_64.tarball.checksum}
/>
)}
{releases.arm64 && releases.arm64.label && (
<DownloadCard
label={releases.arm64.label}
href={releases.arm64.link}
checksum={releases.arm64.checksum}
/>
)}
</div>
</div> </div>
) )
} }

View file

@ -7,55 +7,55 @@ export function getReleasesWithChecksums(checksums: Record<string, string>) {
return { return {
macos: { macos: {
universal: { universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg",
label: 'Universal', label: "Universal",
checksum: checksums['zen.macos-universal.dmg'], checksum: checksums["zen.macos-universal.dmg"],
}, },
}, },
windows: { windows: {
x86_64: { x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe",
label: '64-bit (Recommended)', label: "64-bit (Recommended)",
checksum: checksums['zen.installer.exe'], checksum: checksums["zen.installer.exe"],
}, },
arm64: { arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe",
label: 'ARM64', label: "ARM64",
checksum: checksums['zen.installer-arm64.exe'], checksum: checksums["zen.installer-arm64.exe"],
}, },
}, },
linux: { linux: {
x86_64: { x86_64: {
tarball: { tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz",
label: 'Tarball x86_64', label: "Tarball x86_64",
checksum: checksums['zen.linux-x86_64.tar.xz'], checksum: checksums["zen.linux-x86_64.tar.xz"],
}, },
appImage: { appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage",
label: 'AppImage x86_64', label: "AppImage x86_64",
checksum: checksums['zen-x86_64.AppImage'], checksum: checksums["zen-x86_64.AppImage"],
}, },
}, },
aarch64: { aarch64: {
tarball: { tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz",
label: 'Tarball aarch64', label: "Tarball aarch64",
checksum: checksums['zen.linux-aarch64.tar.xz'], checksum: checksums["zen.linux-aarch64.tar.xz"],
}, },
appImage: { appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage', link: "https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage",
label: 'AppImage aarch64', label: "AppImage aarch64",
checksum: checksums['zen-aarch64.AppImage'], checksum: checksums["zen-aarch64.AppImage"],
}, },
}, },
flathub: { flathub: {
all: { all: {
link: 'https://flathub.org/apps/app.zen_browser.zen', link: "https://flathub.org/apps/app.zen_browser.zen",
label: 'Flathub', label: "Flathub",
}, },
}, },
}, },
} };
} }
--- ---

View file

@ -1,4 +1,4 @@
export const I18N = { export const I18N = {
DEFAULT_LOCALE: 'en', DEFAULT_LOCALE: "en",
LOCALES: [{ label: 'English', value: 'en' }], LOCALES: [{ label: "English", value: "en" }],
} as const } as const;

View file

@ -1,5 +1,5 @@
import { I18N } from './i18n' import { I18N } from "./i18n";
export const CONSTANT = { export const CONSTANT = {
I18N, I18N,
} };

View file

@ -1,169 +1,158 @@
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from "preact/hooks";
import type { ZenTheme } from '../mods' import type { ZenTheme } from "../mods";
type SortOrder = 'default' | 'asc' | 'desc' type SortOrder = "default" | "asc" | "desc";
interface ModsSearchState { interface ModsSearchState {
search: string search: string;
createdSort: SortOrder createdSort: SortOrder;
updatedSort: SortOrder updatedSort: SortOrder;
page: number page: number;
limit: number limit: number;
} }
const DEFAULT_LIMIT = 12 const DEFAULT_LIMIT = 12;
export function useModsSearch(mods: ZenTheme[]) { export function useModsSearch(mods: ZenTheme[]) {
const [searchParams, setSearchParams] = useState<URLSearchParams>() const [searchParams, setSearchParams] = useState<URLSearchParams>();
const [state, setState] = useState<ModsSearchState>({ const [state, setState] = useState<ModsSearchState>({
search: '', search: "",
createdSort: 'desc', createdSort: "desc",
updatedSort: 'default', updatedSort: "default",
page: 1, page: 1,
limit: DEFAULT_LIMIT, limit: DEFAULT_LIMIT,
}) });
// Initialize search params // Initialize search params
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search);
setSearchParams(params) setSearchParams(params);
setState({ setState({
search: params.get('q') || '', search: params.get("q") || "",
createdSort: (params.get('created') as SortOrder) || 'desc', createdSort: (params.get("created") as SortOrder) || "desc",
updatedSort: (params.get('updated') as SortOrder) || 'default', updatedSort: (params.get("updated") as SortOrder) || "default",
page: Number.parseInt(params.get('page') || '1', 10), page: Number.parseInt(params.get("page") || "1", 10),
limit: Number.parseInt(params.get('limit') || String(DEFAULT_LIMIT), 10), limit: Number.parseInt(params.get("limit") || String(DEFAULT_LIMIT), 10),
}) });
}, []) }, []);
// Update URL when state changes // Update URL when state changes
useEffect(() => { useEffect(() => {
if (!searchParams) return if (!searchParams) return;
if (state.search) { if (state.search) {
searchParams.set('q', state.search) searchParams.set("q", state.search);
} else { } else {
searchParams.delete('q') searchParams.delete("q");
} }
if (state.createdSort !== 'default') { if (state.createdSort !== "default") {
searchParams.set('created', state.createdSort) searchParams.set("created", state.createdSort);
} else { } else {
searchParams.delete('created') searchParams.delete("created");
} }
if (state.updatedSort !== 'default') { if (state.updatedSort !== "default") {
searchParams.set('updated', state.updatedSort) searchParams.set("updated", state.updatedSort);
} else { } else {
searchParams.delete('updated') searchParams.delete("updated");
} }
if (state.page > 1) { if (state.page > 1) {
searchParams.set('page', state.page.toString()) searchParams.set("page", state.page.toString());
} else { } else {
searchParams.delete('page') searchParams.delete("page");
} }
if (state.limit !== DEFAULT_LIMIT) { if (state.limit !== DEFAULT_LIMIT) {
searchParams.set('limit', state.limit.toString()) searchParams.set("limit", state.limit.toString());
} else { } else {
searchParams.delete('limit') searchParams.delete("limit");
} }
const newUrl = `${window.location.pathname}${ const newUrl = `${window.location.pathname}${
searchParams.toString() ? `?${searchParams.toString()}` : '' searchParams.toString() ? `?${searchParams.toString()}` : ""
}` }`;
if (state.page > 1) { if (state.page > 1) {
window.history.pushState({}, '', newUrl) window.history.pushState({}, "", newUrl);
} else { } else {
window.history.replaceState({}, '', newUrl) window.history.replaceState({}, "", newUrl);
} }
}, [state, searchParams]) }, [state, searchParams]);
const filteredMods = (() => { const filteredMods = (() => {
let filtered = [...mods] let filtered = [...mods];
// Filter by search // Filter by search
const searchTerm = state.search.toLowerCase() const searchTerm = state.search.toLowerCase();
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter( filtered = filtered.filter(
(mod) => (mod) =>
mod.name.toLowerCase().includes(searchTerm) || mod.name.toLowerCase().includes(searchTerm) ||
mod.description.toLowerCase().includes(searchTerm) || mod.description.toLowerCase().includes(searchTerm) ||
mod.author.toLowerCase().includes(searchTerm) || mod.author.toLowerCase().includes(searchTerm) ||
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? (mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? false),
false), );
)
} }
// Sort by createdAt if chosen // Sort by createdAt if chosen
if (state.createdSort !== 'default') { if (state.createdSort !== "default") {
filtered.sort((a, b) => { filtered.sort((a, b) => {
const diff = const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() return state.createdSort === "asc" ? diff : -diff;
return state.createdSort === 'asc' ? diff : -diff });
})
} }
// Sort by updatedAt if chosen // Sort by updatedAt if chosen
if (state.updatedSort !== 'default') { if (state.updatedSort !== "default") {
filtered.sort((a, b) => { filtered.sort((a, b) => {
const diff = const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() return state.updatedSort === "asc" ? diff : -diff;
return state.updatedSort === 'asc' ? diff : -diff });
})
} }
return filtered return filtered;
})() })();
// Calculate pagination // Calculate pagination
const totalPages = Math.ceil(filteredMods.length / state.limit) const totalPages = Math.ceil(filteredMods.length / state.limit);
const startIndex = (state.page - 1) * state.limit const startIndex = (state.page - 1) * state.limit;
const endIndex = startIndex + state.limit const endIndex = startIndex + state.limit;
const paginatedMods = filteredMods.slice(startIndex, endIndex) const paginatedMods = filteredMods.slice(startIndex, endIndex);
const setSearch = (search: string) => { const setSearch = (search: string) => {
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes setState((prev) => ({ ...prev, search, page: 1 })); // Reset page when search changes
} };
const toggleCreatedSort = () => { const toggleCreatedSort = () => {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
createdSort: createdSort:
prev.createdSort === 'default' prev.createdSort === "default" ? "asc" : prev.createdSort === "asc" ? "desc" : "default",
? 'asc'
: prev.createdSort === 'asc'
? 'desc'
: 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }));
} };
const toggleUpdatedSort = () => { const toggleUpdatedSort = () => {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
updatedSort: updatedSort:
prev.updatedSort === 'default' prev.updatedSort === "default" ? "asc" : prev.updatedSort === "asc" ? "desc" : "default",
? 'asc'
: prev.updatedSort === 'asc'
? 'desc'
: 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }));
} };
const setPage = (page: number) => { const setPage = (page: number) => {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
page: Math.max(1, Math.min(page, totalPages)), page: Math.max(1, Math.min(page, totalPages)),
})) }));
} };
const setLimit = (limit: number) => { const setLimit = (limit: number) => {
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes setState((prev) => ({ ...prev, limit, page: 1 })); // Reset page when limit changes
} };
return { return {
search: state.search, search: state.search,
@ -180,5 +169,5 @@ export function useModsSearch(mods: ZenTheme[]) {
setLimit, setLimit,
mods: paginatedMods, mods: paginatedMods,
searchParams, searchParams,
} };
} }

View file

@ -1,15 +1,15 @@
--- ---
interface Props { interface Props {
title: string; title: string;
description?: string; description?: string;
ogImage?: string; ogImage?: string;
isHome?: boolean; isHome?: boolean;
redirect?: string; redirect?: string;
} }
const { title, description, ogImage, isHome, redirect } = Astro.props; const { title, description, ogImage, isHome, redirect } = Astro.props;
const defaultDescription = const defaultDescription =
"Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features."; "Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features.";
const defaultOgImage = "/share-pic.png"; const defaultOgImage = "/share-pic.png";
import "@fontsource/bricolage-grotesque/400.css"; import "@fontsource/bricolage-grotesque/400.css";
import "@fontsource/bricolage-grotesque/500.css"; import "@fontsource/bricolage-grotesque/500.css";

View file

@ -1,41 +1,41 @@
import { format } from 'date-fns' import { format } from "date-fns";
export interface ZenTheme { export interface ZenTheme {
name: string name: string;
description: string description: string;
image: string image: string;
downloadUrl: string downloadUrl: string;
id: string id: string;
homepage?: string homepage?: string;
readme: string readme: string;
preferences?: string preferences?: string;
isColorTheme: boolean isColorTheme: boolean;
author: string author: string;
version: string version: string;
tags: string[] tags: string[];
createdAt: Date createdAt: Date;
updatedAt: Date updatedAt: Date;
} }
const THEME_API = 'https://zen-browser.github.io/theme-store/themes.json' const THEME_API = "https://zen-browser.github.io/theme-store/themes.json";
export async function getAllMods(): Promise<ZenTheme[]> { export async function getAllMods(): Promise<ZenTheme[]> {
try { try {
const res = await fetch(THEME_API) const res = await fetch(THEME_API);
const json = await res.json() const json = await res.json();
// convert dict to array // convert dict to array
const mods = Object.keys(json).map((key) => json[key]) const mods = Object.keys(json).map((key) => json[key]);
return mods return mods;
} catch (error) { } catch (error) {
console.error(error) console.error(error);
return [] return [];
} }
} }
export function getAuthorLink(author: string): string { export function getAuthorLink(author: string): string {
return `https://github.com/${author}` return `https://github.com/${author}`;
} }
export function getLocalizedDate(date: Date): string { export function getLocalizedDate(date: Date): string {
return format(date, 'PP') return format(date, "PP");
} }

View file

@ -1,5 +1,5 @@
--- ---
import NotFound from './[...locale]/404.astro' import NotFound from "./[...locale]/404.astro";
--- ---
<NotFound /> <NotFound />

View file

@ -1,16 +1,16 @@
--- ---
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import Title from '~/components/Title.astro' import Title from "~/components/Title.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getLocale, getPath, getUI } from '~/utils/i18n' import { getLocale, getPath, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale);
const { const {
routes: { notFound }, routes: { notFound },
} = getUI(locale) } = getUI(locale);
--- ---
<Layout title={notFound.title}> <Layout title={notFound.title}>

View file

@ -8,8 +8,8 @@ export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro); const locale = getLocale(Astro);
const { const {
routes: { about }, routes: { about },
layout, layout,
} = getUI(locale); } = getUI(locale);
--- ---

View file

@ -1,16 +1,16 @@
--- ---
import { ArrowRight } from 'lucide-astro' import { ArrowRight } from "lucide-astro";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { donate }, routes: { donate },
layout, layout,
} = getUI(locale) } = getUI(locale);
--- ---
<Layout title={layout.donate.title} description={layout.donate.description}> <Layout title={layout.donate.title} description={layout.donate.description}>

View file

@ -1,40 +1,35 @@
--- ---
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import DownloadScript from '~/components/download/DownloadScript.astro' import DownloadScript from "~/components/download/DownloadScript.astro";
import PlatformDownload from '~/components/download/PlatformDownload.astro' import PlatformDownload from "~/components/download/PlatformDownload.astro";
import { getReleasesWithChecksums } from '~/components/download/release-data.astro' import { getReleasesWithChecksums } from "~/components/download/release-data.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getChecksums } from '~/utils/githubChecksums' import { getChecksums } from "~/utils/githubChecksums";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
import { icon, library } from '@fortawesome/fontawesome-svg-core' import { icon, library } from "@fortawesome/fontawesome-svg-core";
import { import { faApple, faGithub, faLinux, faWindows } from "@fortawesome/free-brands-svg-icons";
faApple, import { ExternalLink, Lock } from "lucide-astro";
faGithub,
faLinux,
faWindows,
} from '@fortawesome/free-brands-svg-icons'
import { ExternalLink, Lock } from 'lucide-astro'
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { download }, routes: { download },
layout, layout,
} = getUI(locale) } = getUI(locale);
library.add(faWindows, faLinux, faApple, faGithub) library.add(faWindows, faLinux, faApple, faGithub);
const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' }) const windowsIcon = icon({ prefix: "fab", iconName: "windows" });
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' }) const linuxIcon = icon({ prefix: "fab", iconName: "linux" });
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' }) const appleIcon = icon({ prefix: "fab", iconName: "apple" });
const githubIcon = icon({ prefix: 'fab', iconName: 'github' }) const githubIcon = icon({ prefix: "fab", iconName: "github" });
const checksums = await getChecksums() const checksums = await getChecksums();
const releases = getReleasesWithChecksums(checksums) const releases = getReleasesWithChecksums(checksums);
const platformNames = download.platformNames const platformNames = download.platformNames;
const platformDescriptions = download.platformDescriptions const platformDescriptions = download.platformDescriptions;
--- ---
<DownloadScript /> <DownloadScript />

View file

@ -1,25 +1,23 @@
import rss, { type RSSOptions } from '@astrojs/rss' import rss, { type RSSOptions } from "@astrojs/rss";
import { releaseNotes } from '~/release-notes' import { releaseNotes } from "~/release-notes";
import type { ReleaseNote } from '~/release-notes' import type { ReleaseNote } from "~/release-notes";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
/** The default number of entries to include in the RSS feed. */ /** The default number of entries to include in the RSS feed. */
const RSS_ENTRY_LIMIT = 20 const RSS_ENTRY_LIMIT = 20;
/** /**
* Handles the GET request for the `feed.xml` endpoint. * Handles the GET request for the `feed.xml` endpoint.
* @returns The RSS feed for the Zen Browser release notes. * @returns The RSS feed for the Zen Browser release notes.
*/ */
export function GET(context: any) { export function GET(context: { url: URL }) {
// Just in case the release notes array is empty for whatever reason. // Just in case the release notes array is empty for whatever reason.
const latestDate = const latestDate =
releaseNotes.length > 0 releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date();
? formatRssDate(releaseNotes[0].date as string)
: new Date()
const rssData: RSSOptions = { const rssData: RSSOptions = {
title: 'Zen Browser Release Notes', title: "Zen Browser Release Notes",
description: 'Release Notes for the Zen Browser', description: "Release Notes for the Zen Browser",
site: context.url, site: context.url,
items: [], items: [],
customData: ` customData: `
@ -33,7 +31,7 @@ export function GET(context: any) {
<link>https://www.zen-browser.app</link> <link>https://www.zen-browser.app</link>
</image> </image>
`, `,
} };
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) { for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
rssData.items.push({ rssData.items.push({
@ -42,10 +40,10 @@ export function GET(context: any) {
pubDate: formatRssDate(releaseNote.date as string), pubDate: formatRssDate(releaseNote.date as string),
description: releaseNote.extra, description: releaseNote.extra,
content: formatReleaseNote(releaseNote), content: formatReleaseNote(releaseNote),
}) });
} }
return rss(rssData) return rss(rssData);
} }
/** /**
@ -56,15 +54,15 @@ export function GET(context: any) {
* @returns The passed in date string as a Date object. * @returns The passed in date string as a Date object.
*/ */
function formatRssDate(dateStr: string) { function formatRssDate(dateStr: string) {
const splitDate = dateStr.split('/') const splitDate = dateStr.split("/");
if (splitDate.length !== 3) { if (splitDate.length !== 3) {
throw new Error('Invalid date format') throw new Error("Invalid date format");
} }
const day = Number(splitDate[0]) const day = Number(splitDate[0]);
const month = Number(splitDate[1]) - 1 const month = Number(splitDate[1]) - 1;
const year = Number(splitDate[2]) const year = Number(splitDate[2]);
return new Date(year, month, day) return new Date(year, month, day);
} }
/** /**
@ -76,99 +74,83 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
let content = `<p> let content = `<p>
If you encounter any issues, please report them on <a href="https://github.com/zen-browser/desktop/issues/">the issues page</a>. If you encounter any issues, please report them on <a href="https://github.com/zen-browser/desktop/issues/">the issues page</a>.
Thanks everyone for your feedback! Thanks everyone for your feedback!
</p>` </p>`;
if (releaseNote.image) { if (releaseNote.image) {
content += `<img src="https://cdn.jsdelivr.net/gh/zen-browser/www/public/releases/${releaseNote.version}.png" content += `<img src="https://cdn.jsdelivr.net/gh/zen-browser/www/public/releases/${releaseNote.version}.png"
alt="Release Image for version ${releaseNote.version}" alt="Release Image for version ${releaseNote.version}"
style="max-width: 30em; width: 100%; border-radius: 0.5rem;" style="max-width: 30em; width: 100%; border-radius: 0.5rem;"
/>` />`;
} }
if (releaseNote.extra) { if (releaseNote.extra) {
content += `<p>${releaseNote.extra.replace(/(\n)/g, '<br />')}</p>` content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`;
} }
content += addReleaseNoteSection( content += addReleaseNoteSection(
'⚠️ Breaking changes', "⚠️ Breaking changes",
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote), releaseNote.breakingChanges?.map(breakingChangeToReleaseNote),
) );
content += addReleaseNoteSection( content += addReleaseNoteSection("✓ Fixes", releaseNote.fixes?.map(fixToReleaseNote));
'✓ Fixes', content += addReleaseNoteSection("🖌 Theme Changes", releaseNote.themeChanges);
releaseNote.fixes?.map(fixToReleaseNote), content += addReleaseNoteSection("⭐ Features", releaseNote.features);
)
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
return content return content;
} }
function addReleaseNoteSection(title: string, items?: string[]): string { function addReleaseNoteSection(title: string, items?: string[]): string {
if (!items) { if (!items) {
return '' return "";
} }
let content = `<h2>${title}</h2>` let content = `<h2>${title}</h2>`;
content += `<ul>` content += "<ul>";
for (const item of items) { for (const item of items) {
if (item && item.length > 0) { if (item && item.length > 0) {
content += `<li>${item}</li>` content += `<li>${item}</li>`;
} }
} }
content += `</ul>` content += "</ul>";
return content return content;
} }
function fixToReleaseNote( function fixToReleaseNote(fix?: Exclude<ReleaseNote["fixes"], undefined>[number]) {
fix?: Exclude<ReleaseNote['fixes'], undefined>[number], if (typeof fix === "string") {
) { return fix;
if (typeof fix === 'string') {
return fix
} }
if (!fix || !fix.description || fix.description.length === 0) { if (!fix || !fix.description || fix.description.length === 0) {
return '' return "";
} }
let note = fix.description let note = fix.description;
if (fix.issue) { if (fix.issue) {
note += ` (<a href="https://github.com/zen-browser/desktop/issues/${fix.issue}" target="_blank">#${fix.issue}</a>)` note += ` (<a href="https://github.com/zen-browser/desktop/issues/${fix.issue}" target="_blank">#${fix.issue}</a>)`;
} }
return note return note;
} }
function breakingChangeToReleaseNote( function breakingChangeToReleaseNote(
breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number], breakingChange?: Exclude<ReleaseNote["breakingChanges"], undefined>[number],
) { ) {
if (typeof breakingChange === 'string') { if (typeof breakingChange === "string") {
return breakingChange return breakingChange;
} }
if ( if (!breakingChange || !breakingChange.description || breakingChange.description.length === 0) {
!breakingChange || return "";
!breakingChange.description ||
breakingChange.description.length === 0
) {
return ''
} }
return `${breakingChange.description} (<a href="${breakingChange.link}" target="_blank">Learn more</a>)` return `${breakingChange.description} (<a href="${breakingChange.link}" target="_blank">Learn more</a>)`;
} }
function pubDate(date?: Date) { function pubDate(date?: Date) {
date ??= new Date() const newDate = date ?? new Date();
const pieces = date.toString().split(' ') const pieces = newDate.toString().split(" ");
const offsetTime = pieces[5].match(/[-+]\d{4}/) const offsetTime = pieces[5].match(/[-+]\d{4}/);
const offset = offsetTime ? offsetTime : pieces[5] const offset = offsetTime ? offsetTime : pieces[5];
const parts = [ const parts = [`${pieces[0]},`, pieces[2], pieces[1], pieces[3], pieces[4], offset];
pieces[0] + ',',
pieces[2],
pieces[1],
pieces[3],
pieces[4],
offset,
]
return parts.join(' ') return parts.join(" ");
} }

View file

@ -1,15 +1,15 @@
--- ---
import Community from '~/components/Community.astro' import Community from "~/components/Community.astro";
import Features from '~/components/Features.astro' import Features from "~/components/Features.astro";
import Hero from '~/components/Hero.astro' import Hero from "~/components/Hero.astro";
import Sponsors from '~/components/Sponsors.astro' import Sponsors from "~/components/Sponsors.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { layout } = getUI(locale) const { layout } = getUI(locale);
--- ---
<Layout <Layout

View file

@ -1,15 +1,15 @@
--- ---
import { ArrowRight, Info } from 'lucide-astro' import { ArrowRight, Info } from "lucide-astro";
import BackButton from '~/components/BackButton.astro' import BackButton from "~/components/BackButton.astro";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getAllMods, getAuthorLink, getLocalizedDate } from '~/mods' import { getAllMods, getAuthorLink, getLocalizedDate } from "~/mods";
import { getUI } from '~/utils/i18n' import { getUI } from "~/utils/i18n";
import { getLocale, getOtherLocales } from '~/utils/i18n' import { getLocale, getOtherLocales } from "~/utils/i18n";
export async function getStaticPaths() { export async function getStaticPaths() {
const mods = await getAllMods() const mods = await getAllMods();
return mods.flatMap((mod) => [ return mods.flatMap((mod) => [
...getOtherLocales().map((locale) => ({ ...getOtherLocales().map((locale) => ({
params: { params: {
@ -31,25 +31,25 @@ export async function getStaticPaths() {
locale: undefined, locale: undefined,
}, },
}, },
]) ]);
} }
// https://github.com/TeaClientMC/Website/blob/7faacc9f8b2c79c74f711d413b155c84faafc00d/src/pages/news/%5B...slug%5D.astro // https://github.com/TeaClientMC/Website/blob/7faacc9f8b2c79c74f711d413b155c84faafc00d/src/pages/news/%5B...slug%5D.astro
const mod = Astro.props const mod = Astro.props;
const dates = { const dates = {
createdAt: getLocalizedDate(mod.createdAt), createdAt: getLocalizedDate(mod.createdAt),
updatedAt: getLocalizedDate(mod.updatedAt), updatedAt: getLocalizedDate(mod.updatedAt),
} };
const locale = getLocale(Astro) const locale = getLocale(Astro as { params: { locale?: string } });
const { const {
routes: { routes: {
mods: { slug }, mods: { slug },
}, },
} = getUI(locale) } = getUI(locale);
--- ---
<Layout <Layout

View file

@ -1,20 +1,20 @@
--- ---
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import ModsList from '~/components/ModsList' import ModsList from "~/components/ModsList";
import { CONSTANT } from '~/constants' import { CONSTANT } from "~/constants";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getAllMods } from '~/mods' import { getAllMods } from "~/mods";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { mods }, routes: { mods },
layout, layout,
} = getUI(locale) } = getUI(locale);
const allMods = (await getAllMods()) || [] const allMods = (await getAllMods()) || [];
--- ---
<Layout title={layout.mods.title}> <Layout title={layout.mods.title}>

View file

@ -1,15 +1,15 @@
--- ---
import Title from '~/components/Title.astro' import Title from "~/components/Title.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { privacyPolicy }, routes: { privacyPolicy },
layout, layout,
} = getUI(locale) } = getUI(locale);
--- ---
<Layout <Layout

View file

@ -1,32 +1,32 @@
--- ---
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { releaseNotes } from '~/release-notes' import { releaseNotes } from "~/release-notes";
import { getStaticPaths as getI18nPaths, getLocale, getUI } from '~/utils/i18n' import { getStaticPaths as getI18nPaths, getLocale, getUI } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { routes: {
releaseNotes: { slug }, releaseNotes: { slug },
}, },
} = getUI(locale) } = getUI(locale);
export async function getStaticPaths() { export async function getStaticPaths() {
const i18nPaths = getI18nPaths() const i18nPaths = getI18nPaths();
return i18nPaths.flatMap(({ params: { locale } }) => [ return i18nPaths.flatMap(({ params: { locale } }) => [
...releaseNotes.map((release: any) => ({ ...releaseNotes.map((release) => ({
params: { slug: release.version, locale }, params: { slug: release.version, locale },
props: { ...release }, props: { ...release },
})), })),
{ {
params: { slug: 'latest', locale }, params: { slug: "latest", locale },
props: { ...releaseNotes[0] }, props: { ...releaseNotes[0] },
}, },
]) ]);
} }
const release = Astro.props const release = Astro.props;
--- ---
<Layout title={slug.title} redirect={`/release-notes#${release.version}`}> <Layout title={slug.title} redirect={`/release-notes#${release.version}`}>

View file

@ -1,23 +1,20 @@
--- ---
import { Modal, ModalBody, ModalHeader } from 'free-astro-components' import { Modal, ModalBody, ModalHeader } from "free-astro-components";
import { ArrowUp } from 'lucide-astro' import { ArrowUp } from "lucide-astro";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import ReleaseNoteItem from '~/components/ReleaseNoteItem.astro' import ReleaseNoteItem from "~/components/ReleaseNoteItem.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { import { releaseNotes as releaseNotesData, releaseNotesTwilight } from "~/release-notes";
releaseNotes as releaseNotesData, import { getLocale, getUI } from "~/utils/i18n";
releaseNotesTwilight, export { getStaticPaths } from "~/utils/i18n";
} from '~/release-notes'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { releaseNotes }, routes: { releaseNotes },
layout, layout,
} = getUI(locale) } = getUI(locale);
--- ---
<Layout title={layout.releaseNotes.title}> <Layout title={layout.releaseNotes.title}>

View file

@ -1,15 +1,15 @@
--- ---
import Features from '~/components/Features.astro' import Features from "~/components/Features.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { welcome }, routes: { welcome },
layout, layout,
} = getUI(locale) } = getUI(locale);
--- ---
<Layout title={layout.welcome.title} description={layout.welcome.description}> <Layout title={layout.welcome.title} description={layout.welcome.description}>

View file

@ -1,32 +1,29 @@
--- ---
import { ArrowRight } from 'lucide-astro' import { ArrowRight } from "lucide-astro";
import Button from '~/components/Button.astro' import Button from "~/components/Button.astro";
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import SocialMediaStrip from '~/components/SocialMediaStrip.astro' import SocialMediaStrip from "~/components/SocialMediaStrip.astro";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import whatsNewVideo from '~/assets/whats-new.mp4' import whatsNewVideo from "~/assets/whats-new.mp4";
import Video from '~/components/Video.astro' import Video from "~/components/Video.astro";
import { releaseNotes } from '~/release-notes' import { releaseNotes } from "~/release-notes";
import whatsNewText from '~/release-notes/whats-new.json' import whatsNewText from "~/release-notes/whats-new.json";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const latestVersion = releaseNotes[0] const latestVersion = releaseNotes[0];
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { whatsNew }, routes: { whatsNew },
layout, layout,
} = getUI(locale) } = getUI(locale);
// Just redirect to the release notes if we are in a patch version // Just redirect to the release notes if we are in a patch version
if ( if (latestVersion.version.split(".").length > 2 && whatsNewText[1] !== latestVersion.version) {
latestVersion.version.split('.').length > 2 && return Astro.redirect(`/release-notes#${latestVersion.version}`);
whatsNewText[1] !== latestVersion.version
) {
return Astro.redirect(`/release-notes#${latestVersion.version}`)
} }
--- ---

View file

@ -1,43 +1,41 @@
import releaseNotesStable from './release-notes/stable.json' import releaseNotesStable from "./release-notes/stable.json";
interface FixWithIssue { interface FixWithIssue {
description: string description: string;
issue?: number issue?: number;
} }
type Fix = string | FixWithIssue type Fix = string | FixWithIssue;
export type BreakingChange = string | { description: string; link: string } export type BreakingChange = string | { description: string; link: string };
export interface ReleaseNote { export interface ReleaseNote {
version: string version: string;
date?: string // optional for twilight date?: string; // optional for twilight
extra?: string extra?: string;
image?: boolean image?: boolean;
fixes?: Fix[] fixes?: Fix[];
features?: string[] features?: string[];
breakingChanges?: BreakingChange[] breakingChanges?: BreakingChange[];
themeChanges?: string[] themeChanges?: string[];
inProgress?: boolean inProgress?: boolean;
workflowId?: number workflowId?: number;
isTwilight?: boolean isTwilight?: boolean;
} }
export const releaseNotes: ReleaseNote[] = releaseNotesStable.reverse() export const releaseNotes: ReleaseNote[] = releaseNotesStable.reverse();
export { default as releaseNotesTwilight } from './release-notes/twilight.json' export { default as releaseNotesTwilight } from "./release-notes/twilight.json";
export function getReleaseNoteFirefoxVersion( export function getReleaseNoteFirefoxVersion(releaseNote: ReleaseNote): string | null {
releaseNote: ReleaseNote,
): string | null {
// Check if "firefox" is on the feature list // Check if "firefox" is on the feature list
for (const feature of releaseNote.features || []) { for (const feature of releaseNote.features || []) {
if (feature.toLowerCase().includes('firefox')) { if (feature.toLowerCase().includes("firefox")) {
// may be X or X.X or X.X.X // may be X or X.X or X.X.X
const match = feature.match(/(\d+(\.\d+){0,2})/) const match = feature.match(/(\d+(\.\d+){0,2})/);
if (match) { if (match) {
return match[0] return match[0];
} }
} }
} }
return null return null;
} }

View file

@ -31,10 +31,7 @@
"version": "1.0.0-a.2", "version": "1.0.0-a.2",
"date": "12/07/2024", "date": "12/07/2024",
"extra": "This release is the second alpha release of the 1.0.0-alpha series. It includes a lot of bug fixes and improvements given the feedback we received from the first alpha release. This release is still not considered stable, but it's a big step towards the first stable release.", "extra": "This release is the second alpha release of the 1.0.0-alpha series. It includes a lot of bug fixes and improvements given the feedback we received from the first alpha release. This release is still not considered stable, but it's a big step towards the first stable release.",
"features": [ "features": ["Added support for macOS aaarch64!", "Some performance improvements"],
"Added support for macOS aaarch64!",
"Some performance improvements"
],
"fixes": [ "fixes": [
{ {
"description": "Fixed rounded corners of browser views for some websites", "description": "Fixed rounded corners of browser views for some websites",
@ -49,18 +46,13 @@
"issue": 50 "issue": 50
} }
], ],
"breakingChanges": [ "breakingChanges": ["Removed support window's stub installer. It's under development."]
"Removed support window's stub installer. It's under development."
]
}, },
{ {
"version": "1.0.0-a.3", "version": "1.0.0-a.3",
"date": "14/07/2024", "date": "14/07/2024",
"extra": "This release is the third alpha release of the 1.0.0-alpha series. One big feature of this release is the new workspaces feature. This feature allows you to create different workspaces with different tabs and configurations. This release also includes a lot of bug fixes and improvements.", "extra": "This release is the third alpha release of the 1.0.0-alpha series. One big feature of this release is the new workspaces feature. This feature allows you to create different workspaces with different tabs and configurations. This release also includes a lot of bug fixes and improvements.",
"features": [ "features": ["Added support for workspaces (Experimental)", "Better support for macOS aarch64"],
"Added support for workspaces (Experimental)",
"Better support for macOS aarch64"
],
"fixes": [ "fixes": [
{ {
"description": "Fixed subwindows not being displayed correctly", "description": "Fixed subwindows not being displayed correctly",
@ -262,9 +254,7 @@
"issue": 89 "issue": 89
} }
], ],
"breakingChanges": [ "breakingChanges": ["Changed the ID for flatpak to io.github.zen_browser.zen"]
"Changed the ID for flatpak to io.github.zen_browser.zen"
]
}, },
{ {
"version": "1.0.0-a.11", "version": "1.0.0-a.11",
@ -310,9 +300,7 @@
"issue": 124 "issue": 124
} }
], ],
"breakingChanges": [ "breakingChanges": ["Changed the ID for AppImage to io.github.zen_browser.zen"]
"Changed the ID for AppImage to io.github.zen_browser.zen"
]
}, },
{ {
"version": "1.0.0-a.12", "version": "1.0.0-a.12",
@ -363,9 +351,7 @@
"version": "1.0.0-a.13", "version": "1.0.0-a.13",
"date": "05/08/2024", "date": "05/08/2024",
"extra": "This is a smaller release to fix some bugs and improve some small details.\n\nIm going to try doing more frequent releases from now on, see how it goes.", "extra": "This is a smaller release to fix some bugs and improve some small details.\n\nIm going to try doing more frequent releases from now on, see how it goes.",
"features": [ "features": ["Allow to remember sidebar width even after collapsing it."],
"Allow to remember sidebar width even after collapsing it."
],
"fixes": [ "fixes": [
{ {
"description": "Task Manager Icon Missing in Flatpak Version", "description": "Task Manager Icon Missing in Flatpak Version",
@ -906,10 +892,7 @@
"description": "Fixed Mods not being applied to every single window opened" "description": "Fixed Mods not being applied to every single window opened"
} }
], ],
"breakingChanges": [ "breakingChanges": ["Removed Galaxy and Dream mods", "Removed the 'legacy-toolbar' preference"],
"Removed Galaxy and Dream mods",
"Removed the 'legacy-toolbar' preference"
],
"themeChanges": [ "themeChanges": [
"Themes will now be able to have string and number values", "Themes will now be able to have string and number values",
"The configuration schema for mods has been updated. All current mods have been updated automatically." "The configuration schema for mods has been updated. All current mods have been updated automatically."
@ -921,9 +904,7 @@
"image": true, "image": true,
"workflowId": 11000317603, "workflowId": 11000317603,
"extra": "This update addresses some significant issues with the previous release.\n\nWe appreciate your patience and support!", "extra": "This update addresses some significant issues with the previous release.\n\nWe appreciate your patience and support!",
"features": [ "features": ["Added a new system for handling keyboard shortcuts"],
"Added a new system for handling keyboard shortcuts"
],
"fixes": [ "fixes": [
{ {
"description": "The New Tab button is not visible", "description": "The New Tab button is not visible",
@ -980,9 +961,7 @@
"Enabled container tabs by default", "Enabled container tabs by default",
"Improved Expand Tabs on Hover layout" "Improved Expand Tabs on Hover layout"
], ],
"themeChanges": [ "themeChanges": ["Toggle inputs will not use the themed tertiary color"],
"Toggle inputs will not use the themed tertiary color"
],
"breakingChanges": [ "breakingChanges": [
"The keyboard shortcuts will be overriden by the defaults ones in this update" "The keyboard shortcuts will be overriden by the defaults ones in this update"
], ],
@ -1279,9 +1258,7 @@
"issue": 2156 "issue": 2156
} }
], ],
"breakingChanges": [ "breakingChanges": ["Removed Show Expand Button option from settings"],
"Removed Show Expand Button option from settings"
],
"themeChanges": [ "themeChanges": [
"The variable '--zen-main-browser-background' will now contain the generated gradient", "The variable '--zen-main-browser-background' will now contain the generated gradient",
"Added the 'unread' attribute for background tabs that haven't been accessed yet" "Added the 'unread' attribute for background tabs that haven't been accessed yet"
@ -1355,9 +1332,7 @@
"issue": 2413 "issue": 2413
} }
], ],
"breakingChanges": [ "breakingChanges": ["Changed the default layout of the customizable UI buttons"],
"Changed the default layout of the customizable UI buttons"
],
"features": [ "features": [
"Added Zen Glance!", "Added Zen Glance!",
"Updated to the latest stable version of Firefox (132.0)", "Updated to the latest stable version of Firefox (132.0)",
@ -1384,9 +1359,7 @@
"description": "Fixed wrong aligment on glance action buttons" "description": "Fixed wrong aligment on glance action buttons"
} }
], ],
"features": [ "features": ["No new features, sorry"]
"No new features, sorry"
]
}, },
{ {
"version": "1.0.1-a.17", "version": "1.0.1-a.17",
@ -2325,9 +2298,7 @@
"Fixed pinning a tab adding them to the essentials container", "Fixed pinning a tab adding them to the essentials container",
"Other small fixes for compact mode not animating properly" "Other small fixes for compact mode not animating properly"
], ],
"features": [ "features": ["localhost and http URL will no longer be trimmed in single toolbar layout"],
"localhost and http URL will no longer be trimmed in single toolbar layout"
],
"workflowId": 13530880093, "workflowId": 13530880093,
"date": "25/02/2025" "date": "25/02/2025"
}, },
@ -2456,9 +2427,7 @@
"version": "1.10.3b", "version": "1.10.3b",
"image": false, "image": false,
"extra": "Terribly sorry for the frequent updates, this emergency release simply updates Firefox, fixing a critical vulnerability in chromium's sandboxing system. You can read more here:\n\n<a href='https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/'>https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/</a>", "extra": "Terribly sorry for the frequent updates, this emergency release simply updates Firefox, fixing a critical vulnerability in chromium's sandboxing system. You can read more here:\n\n<a href='https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/'>https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/</a>",
"features": [ "features": ["Updated Firefox to 136.0.4"],
"Updated Firefox to 136.0.4"
],
"workflowId": 14109635630, "workflowId": 14109635630,
"date": "27/03/2025" "date": "27/03/2025"
}, },

View file

@ -3,29 +3,25 @@
* Returns a mapping from filename to checksum. * Returns a mapping from filename to checksum.
*/ */
export async function getChecksums() { export async function getChecksums() {
const res = await fetch( const res = await fetch("https://api.github.com/repos/zen-browser/desktop/releases/latest", {
'https://api.github.com/repos/zen-browser/desktop/releases/latest', headers: {
{ Accept: "application/vnd.github+json",
headers: { "X-GitHub-Api-Version": "2022-11-28",
Accept: 'application/vnd.github+json', "User-Agent": "zen-browser-checksum-fetcher",
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'zen-browser-checksum-fetcher',
},
}, },
) });
if (!res.ok) if (!res.ok) throw new Error(`Failed to fetch GitHub release: ${res.statusText}`);
throw new Error('Failed to fetch GitHub release: ' + res.statusText) const data = await res.json();
const data = await res.json() const body = data.body as string;
const body = data.body as string
// Extract the checksum block // Extract the checksum block
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/) const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/);
const checksums: Record<string, string> = {} const checksums: Record<string, string> = {};
if (match && match[1]) { if (match?.[1]) {
match[1].split('\n').forEach((line) => { for (const line of match[1].split("\n")) {
const [hash, filename] = line.trim().split(/\s+/, 2) const [hash, filename] = line.trim().split(/\s+/, 2);
if (hash && filename) checksums[filename] = hash if (hash && filename) checksums[filename] = hash;
}) }
} }
return checksums return checksums;
} }

View file

@ -1,67 +1,87 @@
import type { GetStaticPaths } from 'astro' import type { GetStaticPaths } from "astro";
import { CONSTANT } from '~/constants' import { CONSTANT } from "~/constants";
import UI_EN from '~/i18n/en/translation.json' import UI_EN from "~/i18n/en/translation.json";
export type Locale = (typeof locales)[number] export type Locale = (typeof locales)[number];
export const getPath = (locale?: Locale) => (path: string) => { export const getPath = (locale?: Locale) => (path: string) => {
if (locale && !path.startsWith(`/${locale}`)) { if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}` return `/${locale}${path.startsWith("/") ? "" : "/"}${path}`;
} }
return path return path;
} };
export const getLocale = (Astro: any) => { export const getLocale = (Astro: { params?: { locale?: string } }) => {
if (Astro.params.locale) { if (Astro.params?.locale) {
return Astro.params.locale as Locale return Astro.params.locale as Locale;
} }
} return CONSTANT.I18N.DEFAULT_LOCALE as Locale;
};
export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value) export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value);
const otherLocales = CONSTANT.I18N.LOCALES.filter( const otherLocales = CONSTANT.I18N.LOCALES.filter(
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE, ({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE,
) );
export const getOtherLocales = () => otherLocales export const getOtherLocales = () => otherLocales;
export type UI = typeof UI_EN export type UI = typeof UI_EN;
export const ui = { en: UI_EN } export const ui = { en: UI_EN };
export const getUI = (locale?: Locale | string): UI => { export const getUI = (locale?: Locale | string): UI => {
const validLocale = locales.includes(locale as Locale) const validLocale = locales.includes(locale as Locale) ? locale : CONSTANT.I18N.DEFAULT_LOCALE;
? locale const defaultUI = ui[CONSTANT.I18N.DEFAULT_LOCALE];
: CONSTANT.I18N.DEFAULT_LOCALE const localeUI = ui[validLocale as Locale];
const defaultUI = ui[CONSTANT.I18N.DEFAULT_LOCALE]
const localeUI = ui[validLocale as Locale]
function deepMerge<T>(defaultObj: T, overrideObj: Partial<T>): T { function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
if (typeof defaultObj !== 'object' || defaultObj === null) // Handle non-object cases
return (overrideObj ?? defaultObj) as T if (typeof defaultObj !== "object" || defaultObj === null) {
if (typeof overrideObj !== 'object' || overrideObj === null) return (overrideObj ?? defaultObj) as T;
return (overrideObj ?? defaultObj) as T }
const result: any = Array.isArray(defaultObj) if (typeof overrideObj !== "object" || overrideObj === null) {
? [...defaultObj] return (overrideObj ?? defaultObj) as T;
: { ...defaultObj } }
for (const key in defaultObj) {
if (Object.prototype.hasOwnProperty.call(defaultObj, key)) { // Create a new object or array based on the default object's type
result[key] = deepMerge( const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj };
(defaultObj as any)[key],
(overrideObj as any)?.[key], // Merge properties from the default object
) for (const key of Object.keys(defaultObj) as Array<keyof T>) {
const defaultValue = defaultObj[key];
const overrideValue = overrideObj[key];
// Recursively merge nested objects
if (
defaultValue !== null &&
overrideValue !== null &&
typeof defaultValue === "object" &&
typeof overrideValue === "object"
) {
// Type assertion to handle nested merging
(result as Record<keyof T, unknown>)[key] = deepMerge(
defaultValue as object,
overrideValue as Partial<object>,
);
} else if (overrideValue !== undefined) {
// Override with the new value if it exists
(result as Record<keyof T, unknown>)[key] = overrideValue;
} }
} }
for (const key in overrideObj) {
// Add any new properties from overrideObj
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
if (!(key in defaultObj)) { if (!(key in defaultObj)) {
result[key] = (overrideObj as any)[key] (result as Record<keyof T, unknown>)[key] = overrideObj[key];
} }
} }
return result as T
return result as T;
} }
return deepMerge<UI>(defaultUI, localeUI) return deepMerge(defaultUI, localeUI);
} };
export const getStaticPaths = (() => { export const getStaticPaths = (() => {
return [ return [
@ -69,17 +89,17 @@ export const getStaticPaths = (() => {
params: { locale: undefined }, params: { locale: undefined },
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE }, props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
}, },
...CONSTANT.I18N.LOCALES.filter( ...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE, ({ value }) => ({
).map(({ value }) => ({ params: { locale: value },
params: { locale: value }, props: {
props: { locale: value,
locale: value, },
}, }),
})), ),
] ];
}) satisfies GetStaticPaths }) satisfies GetStaticPaths;
export const getLocales = () => { export const getLocales = () => {
return [...locales, ...otherLocales] return [...locales, ...otherLocales];
} };