Initial Commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
dist/
|
||||
1801
Cargo.lock
generated
Normal file
1801
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
Cargo.toml
Normal file
2
Cargo.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[workspace]
|
||||
members = ["server"]
|
||||
101
electron/.gitignore
vendored
Normal file
101
electron/.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.DS_Store
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Webpack
|
||||
.webpack/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
dist/
|
||||
|
||||
# Electron-Forge
|
||||
out/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
5
electron/.prettierignore
Normal file
5
electron/.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
/.vite
|
||||
README.md
|
||||
8
electron/.prettierrc
Normal file
8
electron/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"endOfLine": "auto",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
21
electron/LICENSE
Normal file
21
electron/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Luan Roger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
146
electron/README.md
Normal file
146
electron/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# electron-shadcn
|
||||
|
||||
Electron in all its glory. Everything you will need to develop your beautiful desktop application.
|
||||
|
||||

|
||||
|
||||
## Libs and tools
|
||||
|
||||
To develop a Electron app, you probably will need some UI, test, formatter, style or other kind of library or framework, so let me install and configure some of them to you.
|
||||
|
||||
### Core 🏍️
|
||||
|
||||
- [Electron 38](https://www.electronjs.org)
|
||||
- [Vite 7](https://vitejs.dev)
|
||||
|
||||
### DX 🛠️
|
||||
|
||||
- [TypeScript 5.9](https://www.typescriptlang.org)
|
||||
- [Prettier](https://prettier.io)
|
||||
- [ESLint 9](https://eslint.org)
|
||||
- [Zod 4](https://zod.dev)
|
||||
- [React Query (TanStack)](https://react-query.tanstack.com)
|
||||
|
||||
### UI 🎨
|
||||
|
||||
- [React 19](https://reactjs.org)
|
||||
- [Tailwind 4](https://tailwindcss.com)
|
||||
- [Shadcn UI](https://ui.shadcn.com)
|
||||
- [Geist](https://vercel.com/font) as default font
|
||||
- [i18next](https://www.i18next.com)
|
||||
- [TanStack Router](https://tanstack.com/router)
|
||||
- [Lucide](https://lucide.dev)
|
||||
|
||||
### Test 🧪
|
||||
|
||||
- [Vitest](https://vitest.dev)
|
||||
- [Playwright](https://playwright.dev)
|
||||
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
||||
|
||||
### Packing and distribution 📦
|
||||
|
||||
- [Electron Forge](https://www.electronforge.io)
|
||||
|
||||
### CI/CD 🚀
|
||||
|
||||
- Pre-configured [GitHub Actions workflow](https://github.com/LuanRoger/electron-shadcn/blob/main/.github/workflows/playwright.yml), for test with Playwright
|
||||
|
||||
### Project preferences 🎯
|
||||
|
||||
- Use Context isolation
|
||||
- [React Compiler](https://react.dev/learn/react-compiler) is enabled by default.
|
||||
- `titleBarStyle`: hidden (Using custom title bar)
|
||||
- Geist as default font
|
||||
- Some default styles was applied, check the [`styles`](https://github.com/LuanRoger/electron-shadcn/tree/main/src/styles) directory
|
||||
- React DevTools are installed by default
|
||||
|
||||
## Directory structure
|
||||
|
||||
```plaintext
|
||||
.
|
||||
└── ./src/
|
||||
├── ./src/assets/
|
||||
│ └── ./src/assets/fonts/
|
||||
├── ./src/components/
|
||||
│ ├── ./src/components/template
|
||||
│ └── ./src/components/ui/
|
||||
├── ./src/helpers/
|
||||
│ └── ./src/helpers/ipc/
|
||||
├── ./src/layout/
|
||||
├── ./src/lib/
|
||||
├── ./src/pages/
|
||||
├── ./src/style/
|
||||
└── ./src/tests/
|
||||
```
|
||||
|
||||
- `src/`: Main directory
|
||||
- `assets/`: Store assets like images, fonts, etc.
|
||||
- `components/`: Store UI components
|
||||
- `template/`: Store the all not important components used by the template. It doesn't include the `WindowRegion` or the theme toggler, if you want to start an empty project, you can safely delete this directory.
|
||||
- `ui/`: Store Shadcn UI components (this is the default direcotry used by Shadcn UI)
|
||||
- `helpers/`: Store IPC related functions to be called in the renderer process
|
||||
- `ipc/`: Directory to store IPC context and listener functions
|
||||
- Some implementations are already done, like `theme` and `window` for the custom title bar
|
||||
- `layout/`: Directory to store layout components
|
||||
- `lib/`: Store libraries and other utilities
|
||||
- `pages/`: Store app's pages
|
||||
- `style/`: Store global styles
|
||||
- `tests/`: Store tests (from Vitest and Playwright)
|
||||
|
||||
## NPM script
|
||||
|
||||
To run any of those scripts:
|
||||
|
||||
```bash
|
||||
npm run <script>
|
||||
```
|
||||
|
||||
- `start`: Start the app in development mode
|
||||
- `package`: Package your application into a platform-specific executable bundle and put the result in a folder.
|
||||
- `make`: Generate platform-specific distributables (e.g. .exe, .dmg, etc) of your application for distribution.
|
||||
- `publish`: Electron Forge's way of taking the artifacts generated by the `make` command and sending them to a service somewhere for you to distribute or use as updates.
|
||||
- `lint`: Run ESLint to lint the code
|
||||
- `format`: Run Prettier to check the code (it doesn't change the code)
|
||||
- `format:write`: Run Prettier to format the code
|
||||
- `test`: Run the default unit-test script (Vitest)
|
||||
- `test:watch`: Run the default unit-test script in watch mode (Vitest)
|
||||
- `test:unit`: Run the Vitest tests
|
||||
- `test:e2e`: Run the Playwright tests
|
||||
- `test:all`: Run all tests (Vitest and Playwright)
|
||||
|
||||
> The test scripts involving Playwright require the app be builded before running the tests. So, before run the tests, run the `package`, `make` or `publish` script.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/LuanRoger/electron-shadcn.git
|
||||
```
|
||||
|
||||
Or use it as a template on GitHub
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the app
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Used by
|
||||
|
||||
- [yaste](https://github.com/LuanRoger/yaste) - yaste (Yet another super ₛᵢₘₚₗₑ text editor) is a text editor, that can be used as an alternative to the native text editor of your SO, maybe.
|
||||
- [eletric-drizzle](https://github.com/LuanRoger/electric-drizzle) - shadcn-ui and Drizzle ORM with Electron.
|
||||
- [Wordle Game](https://github.com/masonyekta/wordle-game) - A Wordle game which features interactive gameplay, cross-platform compatibility, and integration with a custom Wordle API for word validation and letter correctness.
|
||||
- [Mehr 🌟](https://github.com/xmannii/MehrLocalChat) - A modern, elegant local AI chatbot application using Electron, React, shadcn/ui, and Ollama.
|
||||
|
||||
> Does you've used this template in your project? Add it here and open a PR.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](https://github.com/LuanRoger/electron-shadcn/blob/main/LICENSE) file for details.
|
||||
20
electron/components.json
Normal file
20
electron/components.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils/tailwind",
|
||||
"ui": "@/components/ui",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
32
electron/eslint.config.mjs
Normal file
32
electron/eslint.config.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import eslintPluginPrettierRecommended from "eslint-config-prettier";
|
||||
import reactCompiler from "eslint-plugin-react-compiler";
|
||||
import path from "node:path";
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const prettierIgnorePath = path.resolve(__dirname, ".prettierignore");
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
includeIgnoreFile(prettierIgnorePath),
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
||||
plugins: {
|
||||
"react-compiler": reactCompiler,
|
||||
},
|
||||
rules: {
|
||||
"react-compiler/react-compiler": "error",
|
||||
},
|
||||
},
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
eslintPluginPrettierRecommended,
|
||||
...tseslint.configs.recommended,
|
||||
];
|
||||
55
electron/forge.config.ts
Normal file
55
electron/forge.config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||
import { MakerDeb } from "@electron-forge/maker-deb";
|
||||
import { MakerRpm } from "@electron-forge/maker-rpm";
|
||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({}),
|
||||
new MakerZIP({}, ["darwin"]),
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({}),
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
build: [
|
||||
{
|
||||
entry: "src/main.ts",
|
||||
config: "vite.main.config.mts",
|
||||
target: "main",
|
||||
},
|
||||
{
|
||||
entry: "src/preload.ts",
|
||||
config: "vite.preload.config.mts",
|
||||
target: "preload",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: "main_window",
|
||||
config: "vite.renderer.config.mts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
electron/forge.env.d.ts
vendored
Normal file
1
electron/forge.env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
|
||||
BIN
electron/images/demo.gif
Normal file
BIN
electron/images/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 886 KiB |
13
electron/index.html
Normal file
13
electron/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>electron-shadcn Template</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
|
||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
<script type="module" src="/src/renderer.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
13495
electron/package-lock.json
generated
Normal file
13495
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
89
electron/package.json
Normal file
89
electron/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "electron-shadcn",
|
||||
"productName": "electron-shadcn Template",
|
||||
"version": "1.0.0",
|
||||
"description": "Electron Forge with shadcn-ui (Vite + Typescript)",
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check .",
|
||||
"format:write": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:all": "vitest run && playwright test"
|
||||
},
|
||||
"author": "ROG <luan.roger.2003@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.3",
|
||||
"@electron-forge/maker-deb": "^7.8.3",
|
||||
"@electron-forge/maker-rpm": "^7.8.3",
|
||||
"@electron-forge/maker-squirrel": "^7.8.3",
|
||||
"@electron-forge/maker-zip": "^7.8.3",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.3",
|
||||
"@electron-forge/plugin-fuses": "^7.8.3",
|
||||
"@electron-forge/plugin-vite": "^7.8.3",
|
||||
"@electron-forge/shared-types": "^7.8.1",
|
||||
"@electron/fuses": "~1.8.0",
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^22.18.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
||||
"electron": "^38.0.0",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-playwright-helpers": "^1.8.2",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"globals": "^16.3.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"vite": "^7.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@icons-pack/react-simple-icons": "^13.7.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
"@tanstack/react-router": "^1.131.35",
|
||||
"@tanstack/router-devtools": "^1.131.35",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"i18next": "^25.5.2",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^4.1.5"
|
||||
}
|
||||
}
|
||||
23
electron/playwright.config.ts
Normal file
23
electron/playwright.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./src/tests/e2e",
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
26
electron/src/App.tsx
Normal file
26
electron/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { syncThemeWithLocal } from "./helpers/theme_helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./localization/i18n";
|
||||
import { updateAppLanguage } from "./helpers/language_helpers";
|
||||
import { router } from "./routes/router";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
syncThemeWithLocal();
|
||||
updateAppLanguage(i18n);
|
||||
}, [i18n]);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById("app")!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
BIN
electron/src/assets/fonts/geist-mono/geist-mono.ttf
Normal file
BIN
electron/src/assets/fonts/geist-mono/geist-mono.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/geist/geist.ttf
Normal file
BIN
electron/src/assets/fonts/geist/geist.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold-italic.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold-italic.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-italic.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-italic.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-regular.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-regular.ttf
Normal file
Binary file not shown.
97
electron/src/components/DragWindowRegion.tsx
Normal file
97
electron/src/components/DragWindowRegion.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
closeWindow,
|
||||
maximizeWindow,
|
||||
minimizeWindow,
|
||||
} from "@/helpers/window_helpers";
|
||||
import { isMacOS } from "@/utils/platform";
|
||||
import React, { type ReactNode } from "react";
|
||||
|
||||
interface DragWindowRegionProps {
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
export default function DragWindowRegion({ title }: DragWindowRegionProps) {
|
||||
return (
|
||||
<div className="flex w-screen items-stretch justify-between">
|
||||
<div className="draglayer w-full">
|
||||
{title && !isMacOS() && (
|
||||
<div className="flex flex-1 p-2 text-xs whitespace-nowrap text-gray-400 select-none">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{isMacOS() && (
|
||||
<div className="flex flex-1 p-2">
|
||||
{/* Maintain the same height but do not display content */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isMacOS() && <WindowButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowButtons() {
|
||||
return (
|
||||
<div className="flex">
|
||||
<button
|
||||
title="Minimize"
|
||||
type="button"
|
||||
className="p-2 hover:bg-slate-300"
|
||||
onClick={minimizeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<rect fill="currentColor" width="10" height="1" x="1" y="6"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
title="Maximize"
|
||||
type="button"
|
||||
className="p-2 hover:bg-slate-300"
|
||||
onClick={maximizeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<rect
|
||||
width="9"
|
||||
height="9"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Close"
|
||||
className="p-2 hover:bg-red-300"
|
||||
onClick={closeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<polygon
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
|
||||
></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
electron/src/components/LangToggle.tsx
Normal file
33
electron/src/components/LangToggle.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
|
||||
import langs from "@/localization/langs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setAppLanguage } from "@/helpers/language_helpers";
|
||||
|
||||
export default function LangToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
const currentLang = i18n.language;
|
||||
|
||||
function onValueChange(value: string) {
|
||||
setAppLanguage(value, i18n);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
onValueChange={onValueChange}
|
||||
value={currentLang}
|
||||
>
|
||||
{langs.map((lang) => (
|
||||
<ToggleGroupItem
|
||||
key={lang.key}
|
||||
value={lang.key}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{`${lang.prefix}`}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
12
electron/src/components/ToggleTheme.tsx
Normal file
12
electron/src/components/ToggleTheme.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Moon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toggleTheme } from "@/helpers/theme_helpers";
|
||||
|
||||
export default function ToggleTheme() {
|
||||
return (
|
||||
<Button onClick={toggleTheme} size="icon">
|
||||
<Moon size={16} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
10
electron/src/components/template/Footer.tsx
Normal file
10
electron/src/components/template/Footer.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="font-tomorrow text-muted-foreground inline-flex justify-between text-[0.7rem] uppercase">
|
||||
<p>Made by LuanRoger - Based in Brazil 🇧🇷</p>
|
||||
<p>Powered by Electron</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
14
electron/src/components/template/InitialIcons.tsx
Normal file
14
electron/src/components/template/InitialIcons.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { SiElectron, SiReact, SiVite } from "@icons-pack/react-simple-icons";
|
||||
|
||||
export default function InitalIcons() {
|
||||
const iconSize = 48;
|
||||
|
||||
return (
|
||||
<div className="inline-flex gap-2">
|
||||
<SiReact size={iconSize} />
|
||||
<SiVite size={iconSize} />
|
||||
<SiElectron size={iconSize} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
electron/src/components/template/NavigationMenu.tsx
Normal file
31
electron/src/components/template/NavigationMenu.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
NavigationMenu as NavigationMenuBase,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "../ui/navigation-menu";
|
||||
|
||||
export default function NavigationMenu() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<NavigationMenuBase className="text-muted-foreground px-2">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/">{t("titleHomePage")}</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/second-page">{t("titleSecondPage")}</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenuBase>
|
||||
);
|
||||
}
|
||||
59
electron/src/components/ui/button.tsx
Normal file
59
electron/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
168
electron/src/components/ui/navigation-menu.tsx
Normal file
168
electron/src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
73
electron/src/components/ui/toggle-group.tsx
Normal file
73
electron/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
45
electron/src/components/ui/toggle.tsx
Normal file
45
electron/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
7
electron/src/helpers/ipc/context-exposer.ts
Normal file
7
electron/src/helpers/ipc/context-exposer.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { exposeThemeContext } from "./theme/theme-context";
|
||||
import { exposeWindowContext } from "./window/window-context";
|
||||
|
||||
export default function exposeContexts() {
|
||||
exposeWindowContext();
|
||||
exposeThemeContext();
|
||||
}
|
||||
8
electron/src/helpers/ipc/listeners-register.ts
Normal file
8
electron/src/helpers/ipc/listeners-register.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { addThemeEventListeners } from "./theme/theme-listeners";
|
||||
import { addWindowEventListeners } from "./window/window-listeners";
|
||||
|
||||
export default function registerListeners(mainWindow: BrowserWindow) {
|
||||
addWindowEventListeners(mainWindow);
|
||||
addThemeEventListeners();
|
||||
}
|
||||
5
electron/src/helpers/ipc/theme/theme-channels.ts
Normal file
5
electron/src/helpers/ipc/theme/theme-channels.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const THEME_MODE_CURRENT_CHANNEL = "theme-mode:current";
|
||||
export const THEME_MODE_TOGGLE_CHANNEL = "theme-mode:toggle";
|
||||
export const THEME_MODE_DARK_CHANNEL = "theme-mode:dark";
|
||||
export const THEME_MODE_LIGHT_CHANNEL = "theme-mode:light";
|
||||
export const THEME_MODE_SYSTEM_CHANNEL = "theme-mode:system";
|
||||
18
electron/src/helpers/ipc/theme/theme-context.ts
Normal file
18
electron/src/helpers/ipc/theme/theme-context.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
THEME_MODE_CURRENT_CHANNEL,
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
THEME_MODE_SYSTEM_CHANNEL,
|
||||
THEME_MODE_TOGGLE_CHANNEL,
|
||||
} from "./theme-channels";
|
||||
|
||||
export function exposeThemeContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
contextBridge.exposeInMainWorld("themeMode", {
|
||||
current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL),
|
||||
toggle: () => ipcRenderer.invoke(THEME_MODE_TOGGLE_CHANNEL),
|
||||
dark: () => ipcRenderer.invoke(THEME_MODE_DARK_CHANNEL),
|
||||
light: () => ipcRenderer.invoke(THEME_MODE_LIGHT_CHANNEL),
|
||||
system: () => ipcRenderer.invoke(THEME_MODE_SYSTEM_CHANNEL),
|
||||
});
|
||||
}
|
||||
33
electron/src/helpers/ipc/theme/theme-listeners.ts
Normal file
33
electron/src/helpers/ipc/theme/theme-listeners.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { nativeTheme } from "electron";
|
||||
import { ipcMain } from "electron";
|
||||
import {
|
||||
THEME_MODE_CURRENT_CHANNEL,
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
THEME_MODE_SYSTEM_CHANNEL,
|
||||
THEME_MODE_TOGGLE_CHANNEL,
|
||||
} from "./theme-channels";
|
||||
|
||||
export function addThemeEventListeners() {
|
||||
ipcMain.handle(THEME_MODE_CURRENT_CHANNEL, () => nativeTheme.themeSource);
|
||||
ipcMain.handle(THEME_MODE_TOGGLE_CHANNEL, () => {
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
nativeTheme.themeSource = "light";
|
||||
} else {
|
||||
nativeTheme.themeSource = "dark";
|
||||
}
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
});
|
||||
ipcMain.handle(
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
() => (nativeTheme.themeSource = "dark"),
|
||||
);
|
||||
ipcMain.handle(
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
() => (nativeTheme.themeSource = "light"),
|
||||
);
|
||||
ipcMain.handle(THEME_MODE_SYSTEM_CHANNEL, () => {
|
||||
nativeTheme.themeSource = "system";
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
});
|
||||
}
|
||||
3
electron/src/helpers/ipc/window/window-channels.ts
Normal file
3
electron/src/helpers/ipc/window/window-channels.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const WIN_MINIMIZE_CHANNEL = "window:minimize";
|
||||
export const WIN_MAXIMIZE_CHANNEL = "window:maximize";
|
||||
export const WIN_CLOSE_CHANNEL = "window:close";
|
||||
14
electron/src/helpers/ipc/window/window-context.ts
Normal file
14
electron/src/helpers/ipc/window/window-context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
WIN_MINIMIZE_CHANNEL,
|
||||
WIN_MAXIMIZE_CHANNEL,
|
||||
WIN_CLOSE_CHANNEL,
|
||||
} from "./window-channels";
|
||||
|
||||
export function exposeWindowContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
contextBridge.exposeInMainWorld("electronWindow", {
|
||||
minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL),
|
||||
maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL),
|
||||
close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL),
|
||||
});
|
||||
}
|
||||
22
electron/src/helpers/ipc/window/window-listeners.ts
Normal file
22
electron/src/helpers/ipc/window/window-listeners.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import {
|
||||
WIN_CLOSE_CHANNEL,
|
||||
WIN_MAXIMIZE_CHANNEL,
|
||||
WIN_MINIMIZE_CHANNEL,
|
||||
} from "./window-channels";
|
||||
|
||||
export function addWindowEventListeners(mainWindow: BrowserWindow) {
|
||||
ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
ipcMain.handle(WIN_CLOSE_CHANNEL, () => {
|
||||
mainWindow.close();
|
||||
});
|
||||
}
|
||||
19
electron/src/helpers/language_helpers.ts
Normal file
19
electron/src/helpers/language_helpers.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { i18n } from "i18next";
|
||||
|
||||
const languageLocalStorageKey = "lang";
|
||||
|
||||
export function setAppLanguage(lang: string, i18n: i18n) {
|
||||
localStorage.setItem(languageLocalStorageKey, lang);
|
||||
i18n.changeLanguage(lang);
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
|
||||
export function updateAppLanguage(i18n: i18n) {
|
||||
const localLang = localStorage.getItem(languageLocalStorageKey);
|
||||
if (!localLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
i18n.changeLanguage(localLang);
|
||||
document.documentElement.lang = localLang;
|
||||
}
|
||||
64
electron/src/helpers/theme_helpers.ts
Normal file
64
electron/src/helpers/theme_helpers.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ThemeMode } from "@/types/theme-mode";
|
||||
|
||||
const THEME_KEY = "theme";
|
||||
|
||||
export interface ThemePreferences {
|
||||
system: ThemeMode;
|
||||
local: ThemeMode | null;
|
||||
}
|
||||
|
||||
export async function getCurrentTheme(): Promise<ThemePreferences> {
|
||||
const currentTheme = await window.themeMode.current();
|
||||
const localTheme = localStorage.getItem(THEME_KEY) as ThemeMode | null;
|
||||
|
||||
return {
|
||||
system: currentTheme,
|
||||
local: localTheme,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setTheme(newTheme: ThemeMode) {
|
||||
switch (newTheme) {
|
||||
case "dark":
|
||||
await window.themeMode.dark();
|
||||
updateDocumentTheme(true);
|
||||
break;
|
||||
case "light":
|
||||
await window.themeMode.light();
|
||||
updateDocumentTheme(false);
|
||||
break;
|
||||
case "system": {
|
||||
const isDarkMode = await window.themeMode.system();
|
||||
updateDocumentTheme(isDarkMode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
}
|
||||
|
||||
export async function toggleTheme() {
|
||||
const isDarkMode = await window.themeMode.toggle();
|
||||
const newTheme = isDarkMode ? "dark" : "light";
|
||||
|
||||
updateDocumentTheme(isDarkMode);
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
}
|
||||
|
||||
export async function syncThemeWithLocal() {
|
||||
const { local } = await getCurrentTheme();
|
||||
if (!local) {
|
||||
setTheme("system");
|
||||
return;
|
||||
}
|
||||
|
||||
await setTheme(local);
|
||||
}
|
||||
|
||||
function updateDocumentTheme(isDarkMode: boolean) {
|
||||
if (!isDarkMode) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
}
|
||||
9
electron/src/helpers/window_helpers.ts
Normal file
9
electron/src/helpers/window_helpers.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export async function minimizeWindow() {
|
||||
await window.electronWindow.minimize();
|
||||
}
|
||||
export async function maximizeWindow() {
|
||||
await window.electronWindow.maximize();
|
||||
}
|
||||
export async function closeWindow() {
|
||||
await window.electronWindow.close();
|
||||
}
|
||||
17
electron/src/layouts/BaseLayout.tsx
Normal file
17
electron/src/layouts/BaseLayout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import DragWindowRegion from "@/components/DragWindowRegion";
|
||||
import NavigationMenu from "@/components/template/NavigationMenu";
|
||||
|
||||
export default function BaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DragWindowRegion title="electron-shadcn" />
|
||||
<NavigationMenu />
|
||||
<main className="h-screen p-2 pb-20">{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
electron/src/localization/i18n.ts
Normal file
22
electron/src/localization/i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: "en",
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
appName: "electron-shadcn",
|
||||
titleHomePage: "Home Page",
|
||||
titleSecondPage: "Second Page",
|
||||
},
|
||||
},
|
||||
"pt-BR": {
|
||||
translation: {
|
||||
appName: "electron-shadcn",
|
||||
titleHomePage: "Página Inicial",
|
||||
titleSecondPage: "Segunda Página",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
14
electron/src/localization/langs.ts
Normal file
14
electron/src/localization/langs.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Language } from "./language";
|
||||
|
||||
export default [
|
||||
{
|
||||
key: "en",
|
||||
nativeName: "English",
|
||||
prefix: "EN-US",
|
||||
},
|
||||
{
|
||||
key: "pt-BR",
|
||||
nativeName: "Português (Brasil)",
|
||||
prefix: "PT-BR",
|
||||
},
|
||||
] satisfies Language[];
|
||||
5
electron/src/localization/language.ts
Normal file
5
electron/src/localization/language.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Language {
|
||||
key: string;
|
||||
nativeName: string;
|
||||
prefix: string;
|
||||
}
|
||||
64
electron/src/main.ts
Normal file
64
electron/src/main.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import registerListeners from "./helpers/ipc/listeners-register";
|
||||
// "electron-squirrel-startup" seems broken when packaging with vite
|
||||
//import started from "electron-squirrel-startup";
|
||||
import path from "path";
|
||||
import {
|
||||
installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
|
||||
const inDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
function createWindow() {
|
||||
const preload = path.join(__dirname, "preload.js");
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
devTools: inDevelopment,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: false,
|
||||
|
||||
preload: preload,
|
||||
},
|
||||
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
|
||||
trafficLightPosition:
|
||||
process.platform === "darwin" ? { x: 5, y: 5 } : undefined,
|
||||
});
|
||||
registerListeners(mainWindow);
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function installExtensions() {
|
||||
try {
|
||||
const result = await installExtension(REACT_DEVELOPER_TOOLS);
|
||||
console.log(`Extensions installed successfully: ${result.name}`);
|
||||
} catch {
|
||||
console.error("Failed to install extensions");
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow).then(installExtensions);
|
||||
|
||||
//osX only
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
//osX only ends
|
||||
30
electron/src/pages/HomePage.tsx
Normal file
30
electron/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import ToggleTheme from "@/components/ToggleTheme";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LangToggle from "@/components/LangToggle";
|
||||
import Footer from "@/components/template/Footer";
|
||||
import InitialIcons from "@/components/template/InitialIcons";
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2">
|
||||
<InitialIcons />
|
||||
<span>
|
||||
<h1 className="font-mono text-4xl font-bold">{t("appName")}</h1>
|
||||
<p
|
||||
className="text-muted-foreground text-end text-sm uppercase"
|
||||
data-testid="pageTitle"
|
||||
>
|
||||
{t("titleHomePage")}
|
||||
</p>
|
||||
</span>
|
||||
<LangToggle />
|
||||
<ToggleTheme />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
electron/src/pages/SecondPage.tsx
Normal file
16
electron/src/pages/SecondPage.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import Footer from "@/components/template/Footer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SecondPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2">
|
||||
<h1 className="text-4xl font-bold">{t("titleSecondPage")}</h1>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
electron/src/preload.ts
Normal file
3
electron/src/preload.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import exposeContexts from "./helpers/ipc/context-exposer";
|
||||
|
||||
exposeContexts();
|
||||
1
electron/src/renderer.ts
Normal file
1
electron/src/renderer.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@/App";
|
||||
15
electron/src/routes/__root.tsx
Normal file
15
electron/src/routes/__root.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import BaseLayout from "@/layouts/BaseLayout";
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
|
||||
export const RootRoute = createRootRoute({
|
||||
component: Root,
|
||||
});
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Outlet />
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
13
electron/src/routes/router.tsx
Normal file
13
electron/src/routes/router.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createMemoryHistory, createRouter } from "@tanstack/react-router";
|
||||
import { rootTree } from "./routes";
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ["/"],
|
||||
});
|
||||
export const router = createRouter({ routeTree: rootTree, history: history });
|
||||
37
electron/src/routes/routes.tsx
Normal file
37
electron/src/routes/routes.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createRoute } from "@tanstack/react-router";
|
||||
import { RootRoute } from "./__root";
|
||||
import HomePage from "../pages/HomePage";
|
||||
import SecondPage from "@/pages/SecondPage";
|
||||
|
||||
// TODO: Steps to add a new route:
|
||||
// 1. Create a new page component in the '../pages/' directory (e.g., NewPage.tsx)
|
||||
// 2. Import the new page component at the top of this file
|
||||
// 3. Define a new route for the page using createRoute()
|
||||
// 4. Add the new route to the routeTree in RootRoute.addChildren([...])
|
||||
// 5. Add a new Link in the navigation section of RootRoute if needed
|
||||
|
||||
// Example of adding a new route:
|
||||
// 1. Create '../pages/NewPage.tsx'
|
||||
// 2. Import: import NewPage from '../pages/NewPage';
|
||||
// 3. Define route:
|
||||
// const NewRoute = createRoute({
|
||||
// getParentRoute: () => RootRoute,
|
||||
// path: '/new',
|
||||
// component: NewPage,
|
||||
// });
|
||||
// 4. Add to routeTree: RootRoute.addChildren([HomeRoute, NewRoute, ...])
|
||||
// 5. Add Link: <Link to="/new">New Page</Link>
|
||||
|
||||
export const HomeRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
path: "/",
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
export const SecondPageRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
path: "/second-page",
|
||||
component: SecondPage,
|
||||
});
|
||||
|
||||
export const rootTree = RootRoute.addChildren([HomeRoute, SecondPageRoute]);
|
||||
203
electron/src/styles/global.css
Normal file
203
electron/src/styles/global.css
Normal file
@@ -0,0 +1,203 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--font-tomorrow: Tomorrow, sans-serif;
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= --theme(--breakpoint-sm)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 1400px) {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
|
||||
src: url("../assets/fonts/geist/geist.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Mono";
|
||||
font-display: swap;
|
||||
|
||||
src: url("../assets/fonts/geist-mono/geist-mono.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-regular.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-italic.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-bold.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-bold-italic.ttf")
|
||||
format("truetype");
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
.draglayer {
|
||||
@apply bg-background;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
button {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
51
electron/src/tests/e2e/example.test.ts
Normal file
51
electron/src/tests/e2e/example.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page,
|
||||
} from "@playwright/test";
|
||||
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
|
||||
|
||||
/*
|
||||
* Using Playwright with Electron:
|
||||
* https://www.electronjs.org/pt/docs/latest/tutorial/automated-testing#using-playwright
|
||||
*/
|
||||
|
||||
let electronApp: ElectronApplication;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const latestBuild = findLatestBuild();
|
||||
const appInfo = parseElectronApp(latestBuild);
|
||||
process.env.CI = "e2e";
|
||||
|
||||
electronApp = await electron.launch({
|
||||
args: [appInfo.main],
|
||||
});
|
||||
electronApp.on("window", async (page) => {
|
||||
const filename = page.url()?.split("/").pop();
|
||||
console.log(`Window opened: ${filename}`);
|
||||
|
||||
page.on("pageerror", (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
page.on("console", (msg) => {
|
||||
console.log(msg.text());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("renders the first page", async () => {
|
||||
const page: Page = await electronApp.firstWindow();
|
||||
const title = await page.waitForSelector("h1");
|
||||
const text = await title.textContent();
|
||||
expect(text).toBe("electron-shadcn");
|
||||
});
|
||||
|
||||
test("renders page name", async () => {
|
||||
const page: Page = await electronApp.firstWindow();
|
||||
await page.waitForSelector("h1");
|
||||
const pageName = await page.getByTestId("pageTitle");
|
||||
const text = await pageName.textContent();
|
||||
expect(text).toBe("Home Page");
|
||||
});
|
||||
27
electron/src/tests/unit/ToggleTheme.test.tsx
Normal file
27
electron/src/tests/unit/ToggleTheme.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { test, expect } from "vitest";
|
||||
import ToggleTheme from "@/components/ToggleTheme";
|
||||
import React from "react";
|
||||
|
||||
test("renders ToggleTheme", () => {
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const isButton = getByRole("button");
|
||||
|
||||
expect(isButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("has icon", () => {
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const button = getByRole("button");
|
||||
const icon = button.querySelector("svg");
|
||||
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("is moon icon", () => {
|
||||
const svgIconClassName: string = "lucide-moon";
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const svg = getByRole("button").querySelector("svg");
|
||||
|
||||
expect(svg?.classList).toContain(svgIconClassName);
|
||||
});
|
||||
1
electron/src/tests/unit/setup.ts
Normal file
1
electron/src/tests/unit/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
14
electron/src/tests/unit/sum.test.ts
Normal file
14
electron/src/tests/unit/sum.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
test("sum", () => {
|
||||
const param1: number = 2;
|
||||
const param2: number = 2;
|
||||
|
||||
const result: number = sum(param1, param2);
|
||||
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
24
electron/src/types.d.ts
vendored
Normal file
24
electron/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
|
||||
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
|
||||
// whether you're running in development or production).
|
||||
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
declare const MAIN_WINDOW_VITE_NAME: string;
|
||||
|
||||
// Preload types
|
||||
interface ThemeModeContext {
|
||||
toggle: () => Promise<boolean>;
|
||||
dark: () => Promise<void>;
|
||||
light: () => Promise<void>;
|
||||
system: () => Promise<boolean>;
|
||||
current: () => Promise<"dark" | "light" | "system">;
|
||||
}
|
||||
interface ElectronWindow {
|
||||
minimize: () => Promise<void>;
|
||||
maximize: () => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
themeMode: ThemeModeContext;
|
||||
electronWindow: ElectronWindow;
|
||||
}
|
||||
1
electron/src/types/theme-mode.ts
Normal file
1
electron/src/types/theme-mode.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ThemeMode = "dark" | "light" | "system";
|
||||
13
electron/src/utils/platform.ts
Normal file
13
electron/src/utils/platform.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Platform detection utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the current platform is macOS
|
||||
* @returns true if running on macOS, false otherwise
|
||||
*/
|
||||
export const isMacOS = (): boolean => {
|
||||
return (
|
||||
typeof window !== "undefined" && window.navigator.platform.includes("Mac")
|
||||
);
|
||||
};
|
||||
6
electron/src/utils/tailwind.ts
Normal file
6
electron/src/utils/tailwind.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
32
electron/tsconfig.json
Normal file
32
electron/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["dom", "ESNext"],
|
||||
"experimentalDecorators": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": false,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"./package.json",
|
||||
"./forge.config.ts",
|
||||
"*.mts",
|
||||
"vite.renderer.config.mts"
|
||||
]
|
||||
}
|
||||
11
electron/vite.main.config.mts
Normal file
11
electron/vite.main.config.mts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
4
electron/vite.preload.config.mts
Normal file
4
electron/vite.preload.config.mts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({});
|
||||
21
electron/vite.renderer.config.mts
Normal file
21
electron/vite.renderer.config.mts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [["babel-plugin-react-compiler"]],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
preserveSymlinks: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
26
electron/vitest.config.ts
Normal file
26
electron/vitest.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
dir: "./src/tests/unit",
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/tests/unit/setup.ts",
|
||||
css: true,
|
||||
reporters: ["verbose"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
include: ["src/**/*"],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
30
server/Cargo.toml
Normal file
30
server/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# web
|
||||
tower-livereload = "0.9.6"
|
||||
socketioxide-core = "0.16.0"
|
||||
socketioxide = "0.16.0"
|
||||
tower-http = { version = "0.6.2", features = ["cors", "trace", "fs"] }
|
||||
axum = { version = "0.8.1", features = ["macros"] }
|
||||
|
||||
# utility
|
||||
uom = "0.36.0"
|
||||
chrono = "0.4.39"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
serde = "1.0.217"
|
||||
anyhow = "1.0.95"
|
||||
serde_json = "1.0.137"
|
||||
signal-hook = "0.3.17"
|
||||
log = "0.4.25"
|
||||
env_logger = "0.11.6"
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread"] }
|
||||
include_dir = "0.7.4"
|
||||
mime_guess = "2.0.5"
|
||||
open = "5.3.2"
|
||||
bitvec = "1.0.1"
|
||||
lazy_static = "1.5.0"
|
||||
10
server/rust-toolchain.toml
Normal file
10
server/rust-toolchain.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[toolchain]
|
||||
channel = "beta"
|
||||
components = [
|
||||
"rustfmt",
|
||||
"clippy",
|
||||
"rust-src",
|
||||
"rustc-dev",
|
||||
"llvm-tools-preview",
|
||||
"rust-analyzer",
|
||||
]
|
||||
1
server/src/app_state.rs
Normal file
1
server/src/app_state.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub static APP_STATE: LazyLock<Arc<AppState>> = LazyLock::new(|| Arc::new(AppState::new()));
|
||||
15
server/src/main.rs
Normal file
15
server/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use anyhow::{Error, Result};
|
||||
//use app_state::APP_STATE;
|
||||
use env_logger::Env;
|
||||
use rest::init::init_api;
|
||||
//use socketio::init::init_socketio;
|
||||
|
||||
pub mod rest;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
init_api().await?;
|
||||
Ok(())
|
||||
}
|
||||
32
server/src/rest/handlers/mod.rs
Normal file
32
server/src/rest/handlers/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use axum::body::Body;
|
||||
use serde::Serialize;
|
||||
|
||||
pub mod my_test;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct MutationResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl MutationResponse {
|
||||
pub fn success() -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
pub fn error(error: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MutationResponse> for Body {
|
||||
fn from(mutation_response: MutationResponse) -> Self {
|
||||
let body = serde_json::to_string(&mutation_response).unwrap();
|
||||
Body::from(body)
|
||||
}
|
||||
}
|
||||
10
server/src/rest/handlers/my_test.rs
Normal file
10
server/src/rest/handlers/my_test.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use super::MutationResponse;
|
||||
use crate::rest::util::ResponseUtil;
|
||||
|
||||
use axum::{body::Body, extract::State, http::Response, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn post_my_test() -> Response<Body> {
|
||||
ResponseUtil::ok(MutationResponse::success())
|
||||
}
|
||||
24
server/src/rest/init.rs
Normal file
24
server/src/rest/init.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use super::handlers::my_test::post_my_test;
|
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||
use std::io::Error;
|
||||
use axum::routing::post;
|
||||
|
||||
pub async fn init_api(
|
||||
|
||||
) -> Result<(), Error> {
|
||||
let cors = CorsLayer::permissive();
|
||||
let app = axum::Router::new()
|
||||
.route(
|
||||
"/api/v1/test",
|
||||
post(post_my_test)
|
||||
)
|
||||
.layer(cors)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
open::that("http://localhost:3001")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
server/src/rest/mod.rs
Normal file
3
server/src/rest/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod handlers;
|
||||
pub mod init;
|
||||
pub mod util;
|
||||
51
server/src/rest/util.rs
Normal file
51
server/src/rest/util.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Response, StatusCode},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct ResponseUtil {}
|
||||
|
||||
impl ResponseUtil {
|
||||
pub fn error(message: &str) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&json!({ "error": message })).unwrap(),
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn ok<T: serde::Serialize>(data: T) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&data).unwrap()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn not_found(message: &str) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&json!({ "error": message })).unwrap(),
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ResponseUtilError {
|
||||
Error(anyhow::Error),
|
||||
NotFound(anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<ResponseUtilError> for Response<Body> {
|
||||
fn from(error: ResponseUtilError) -> Self {
|
||||
match error {
|
||||
ResponseUtilError::Error(e) => ResponseUtil::error(&e.to_string()),
|
||||
ResponseUtilError::NotFound(e) => ResponseUtil::not_found(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user