diff --git a/.env.example b/.env.example
deleted file mode 100644
index 63eee96..0000000
--- a/.env.example
+++ /dev/null
@@ -1 +0,0 @@
-VITE_API_URL=http://localhost:5050/api/v1
diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml
index db9e331..a99d512 100644
--- a/.github/actions/setup-node/action.yml
+++ b/.github/actions/setup-node/action.yml
@@ -2,12 +2,12 @@ name: Setup Node.js
description: Setup Node.js
runs:
- using: "composite"
- steps:
- - uses: actions/setup-node@v4
- name: Setup Node.js
- with:
- node-version-file: .nvmrc
- - name: Install dependencies
- run: npm i
- shell: bash
+ using: "composite"
+ steps:
+ - uses: actions/setup-node@v4
+ name: Setup Node.js
+ with:
+ node-version-file: .nvmrc
+ - name: Install dependencies
+ run: npm i
+ shell: bash
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..8695e39
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,63 @@
+name: Publish
+
+on:
+ push:
+ branches: [main]
+ tags: [v*]
+ pull_request:
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+ DOCKER_USER: ${{ github.actor }}
+ DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+ steps:
+ - name: Install Cosign
+ uses: sigstore/cosign-installer@v3.8.0
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Login to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ env.DOCKER_USER }}
+ password: ${{ env.DOCKER_PASSWORD }}
+ - name: Get metadata
+ uses: docker/metadata-action@v4
+ id: meta
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=sha
+ - name: Build and push
+ uses: docker/build-push-action@v4
+ id: build-and-push
+ with:
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ cache-to: type=inline
+ - name: Sign the published Docker image
+ if: ${{ github.event_name != 'pull_request' }}
+ env:
+ TAGS: ${{ steps.meta.outputs.tags }}
+ DIGEST: ${{ steps.build-and-push.outputs.digest }}
+ run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f333619..5ff067a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,13 @@
## [1.1.0](https://github.com/thangved/react-boilerplate/compare/v1.0.7...v1.1.0) (2024-12-17)
-
### Features
-* add i18next ([05f925b](https://github.com/thangved/react-boilerplate/commit/05f925be508b94f7f1ab300b09c690c70d1dea7d))
-
+- add i18next ([05f925b](https://github.com/thangved/react-boilerplate/commit/05f925be508b94f7f1ab300b09c690c70d1dea7d))
### Bug Fixes
-* **deps:** update react monorepo to v19 ([95fe746](https://github.com/thangved/react-boilerplate/commit/95fe7469d5116af762419e35fd97cce8e08b7d5d))
+- **deps:** update react monorepo to v19 ([95fe746](https://github.com/thangved/react-boilerplate/commit/95fe7469d5116af762419e35fd97cce8e08b7d5d))
## [1.0.7](https://github.com/thangved/react-boilerplate/compare/v1.0.6...v1.0.7) (2024-12-06)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..a2ec9e9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+FROM node:22 AS base
+
+FROM base AS builder
+
+WORKDIR /app
+
+ADD ./package.json ./
+
+RUN npm i --ignore-scripts
+
+ADD ./locales ./locales
+ADD ./public ./public
+ADD ./src ./src
+ADD ./index.html\
+ ./tsconfig.json\
+ ./tsconfig.node.json\
+ ./vite.config.ts\
+ ./
+
+RUN npm run build
+
+FROM nginx:alpine AS runner
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+ADD ./nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..246b0ca
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,10 @@
+services:
+ web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ image: ghrc.io/thangved/react-boilerplate
+ ports:
+ - 8888:80
+ api:
+ image: ghrc.io/thangved/express0
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 2b5389c..bc7f7e0 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,42 +1,47 @@
import { fixupConfigRules } from "@eslint/compat";
+import { FlatCompat } from "@eslint/eslintrc";
+import js from "@eslint/js";
+import tsParser from "@typescript-eslint/parser";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
-import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
-import js from "@eslint/js";
-import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended,
- allConfig: js.configs.all
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
});
-export default [{
- ignores: ["**/dist", "**/.eslintrc.cjs"],
-}, ...fixupConfigRules(compat.extends(
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended",
- "plugin:react-hooks/recommended",
-)), {
- plugins: {
- "react-refresh": reactRefresh,
- },
+export default [
+ {
+ ignores: ["**/dist", "**/.eslintrc.cjs"],
+ },
+ ...fixupConfigRules(
+ compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"),
+ ),
+ {
+ plugins: {
+ "react-refresh": reactRefresh,
+ },
- languageOptions: {
- globals: {
- ...globals.browser,
- },
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ },
- parser: tsParser,
- },
+ parser: tsParser,
+ },
- rules: {
- "react-refresh/only-export-components": ["warn", {
- allowConstantExport: true,
- }],
- },
-}];
\ No newline at end of file
+ rules: {
+ "react-refresh/only-export-components": [
+ "warn",
+ {
+ allowConstantExport: true,
+ },
+ ],
+ },
+ },
+];
diff --git a/index.html b/index.html
index 3f6de4c..2586014 100644
--- a/index.html
+++ b/index.html
@@ -59,7 +59,7 @@
.app-logo {
width: 150px;
height: 150px;
- background: url(@/assets/logo.svg) no-repeat center center / contain;
+ background: url(/logo.svg) no-repeat center center / contain;
animation: blingbling 0.5s infinite;
position: relative;
z-index: 10;
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..256a63d
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,17 @@
+server {
+ listen 80;
+
+ location /api {
+ proxy_pass http://api:3000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index b4e1ab0..c052fb9 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "react-boilerplate",
"version": "1.1.0",
"private": true,
+ "license": "MIT",
"type": "module",
"scripts": {
"build": "tsc && vite build",
@@ -22,8 +23,6 @@
"clsx": "^2.1.1",
"i18next": "^24.1.1",
"i18next-browser-languagedetector": "^8.0.2",
- "i18next-chained-backend": "^4.6.2",
- "i18next-http-backend": "^3.0.1",
"i18next-localstorage-backend": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -31,10 +30,7 @@
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.2",
"sharp": "^0.33.5",
- "svgo": "^3.3.2",
- "translation-check": "^1.1.0",
- "vite-plugin-bundle-prefetch": "^0.0.4",
- "vite-plugin-image-optimizer": "^1.1.8"
+ "svgo": "^3.3.2"
},
"devDependencies": {
"@commitlint/cli": "^19.6.1",
diff --git a/public/apple-touch-icon-180x180.png b/public/apple-touch-icon-180x180.png
index 74e32c8..cbf148b 100644
Binary files a/public/apple-touch-icon-180x180.png and b/public/apple-touch-icon-180x180.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
index 0867fc1..5d420bc 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/logo.svg b/public/logo.svg
index d9cf046..f9d50b7 100644
--- a/public/logo.svg
+++ b/public/logo.svg
@@ -1,11 +1,5 @@
-
\ No newline at end of file
+
+
diff --git a/public/maskable-icon-512x512.png b/public/maskable-icon-512x512.png
index 6c30546..8d6cd63 100644
Binary files a/public/maskable-icon-512x512.png and b/public/maskable-icon-512x512.png differ
diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png
index 8d4c09d..1ed7535 100644
Binary files a/public/pwa-192x192.png and b/public/pwa-192x192.png differ
diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png
index 30cb3ea..a0a9d39 100644
Binary files a/public/pwa-512x512.png and b/public/pwa-512x512.png differ
diff --git a/public/pwa-64x64.png b/public/pwa-64x64.png
index 68aca83..6f54817 100644
Binary files a/public/pwa-64x64.png and b/public/pwa-64x64.png differ
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
deleted file mode 100644
index d9cf046..0000000
--- a/src/assets/logo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/AppLogo/AppLogo.module.scss b/src/components/AppLogo/AppLogo.module.scss
new file mode 100644
index 0000000..fa765a6
--- /dev/null
+++ b/src/components/AppLogo/AppLogo.module.scss
@@ -0,0 +1,5 @@
+.wrapper {
+ width: 150px;
+ height: 150px;
+ background: url(/logo.svg) no-repeat center center / contain;
+}
diff --git a/src/components/AppLogo/AppLogo.tsx b/src/components/AppLogo/AppLogo.tsx
new file mode 100644
index 0000000..b2860d8
--- /dev/null
+++ b/src/components/AppLogo/AppLogo.tsx
@@ -0,0 +1,9 @@
+import clsx from "clsx";
+import React from "react";
+import styles from "./AppLogo.module.scss";
+
+export type AppLogoProps = React.HTMLAttributes;
+
+export default function AppLogo({ className, ...props }: AppLogoProps) {
+ return ;
+}
diff --git a/src/components/AppLogo/index.ts b/src/components/AppLogo/index.ts
new file mode 100644
index 0000000..c038fcc
--- /dev/null
+++ b/src/components/AppLogo/index.ts
@@ -0,0 +1 @@
+export { default } from "./AppLogo";
diff --git a/src/components/first-loader/first-loader.module.scss b/src/components/FirstLoader/FirstLoader.module.scss
similarity index 100%
rename from src/components/first-loader/first-loader.module.scss
rename to src/components/FirstLoader/FirstLoader.module.scss
diff --git a/src/components/FirstLoader/FirstLoader.tsx b/src/components/FirstLoader/FirstLoader.tsx
new file mode 100644
index 0000000..ab02212
--- /dev/null
+++ b/src/components/FirstLoader/FirstLoader.tsx
@@ -0,0 +1,10 @@
+import AppLogo from "../AppLogo";
+import styles from "./FirstLoader.module.scss";
+
+export default function FirstLoader() {
+ return (
+
+ );
+}
diff --git a/src/components/FirstLoader/index.ts b/src/components/FirstLoader/index.ts
new file mode 100644
index 0000000..4db2c72
--- /dev/null
+++ b/src/components/FirstLoader/index.ts
@@ -0,0 +1 @@
+export { default } from "./FirstLoader";
diff --git a/src/components/first-loader/index.tsx b/src/components/first-loader/index.tsx
deleted file mode 100644
index 237656b..0000000
--- a/src/components/first-loader/index.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Logo from "../logo";
-import styles from "./first-loader.module.scss";
-
-export default function FirstLoader() {
- return (
-
-
-
- );
-}
diff --git a/src/components/logo/index.tsx b/src/components/logo/index.tsx
deleted file mode 100644
index 6e1997d..0000000
--- a/src/components/logo/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import clsx from "clsx";
-import React from "react";
-import styles from "./logo.module.scss";
-
-export type LogoProps = React.HTMLAttributes;
-
-export default function Logo({ className, ...props }: LogoProps) {
- return ;
-}
diff --git a/src/components/logo/logo.module.scss b/src/components/logo/logo.module.scss
deleted file mode 100644
index 5a42a12..0000000
--- a/src/components/logo/logo.module.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.wrapper {
- width: 150px;
- height: 150px;
- background: url(@/assets/logo.svg) no-repeat center center / contain;
-}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..1a9829c
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from "./useAppDispatch";
+export * from "./useAppSelector";
diff --git a/src/hooks/useAppDispatch.ts b/src/hooks/useAppDispatch.ts
index 4c76064..5c02194 100644
--- a/src/hooks/useAppDispatch.ts
+++ b/src/hooks/useAppDispatch.ts
@@ -2,5 +2,4 @@ import store from "@/store";
import { useDispatch } from "react-redux";
export type AppDispatch = typeof store.dispatch;
-const useAppDispatch = useDispatch.withTypes();
-export default useAppDispatch;
+export const useAppDispatch = useDispatch.withTypes();
diff --git a/src/hooks/useAppSelector.ts b/src/hooks/useAppSelector.ts
index cf55380..c0672c3 100644
--- a/src/hooks/useAppSelector.ts
+++ b/src/hooks/useAppSelector.ts
@@ -2,5 +2,4 @@ import store from "@/store";
import { useSelector } from "react-redux";
export type RootState = ReturnType;
-const useAppSelector = useSelector.withTypes();
-export default useAppSelector;
+export const useAppSelector = useSelector.withTypes();
diff --git a/src/http/http.ts b/src/http/http.ts
deleted file mode 100644
index a4d5def..0000000
--- a/src/http/http.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import tokenService from "@/services/token.service";
-import axios from "axios";
-
-/**
- * Create an Axios instance for making HTTP requests
- * @param resource Resource path for the service
- * @example http("users") => http://localhost:3000/users
- * @returns Axios instance
- */
-export default function http(resource = "") {
- const http = axios.create({
- baseURL: `${import.meta.env.VITE_API_URL}/${resource}`,
- });
-
- // Add a request interceptor
- http.interceptors.request.use((config) => {
- // Get the access token from the token service
- const accessToken = tokenService.accessToken;
- // If the access token exists, add it to the Authorization header
- if (accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
- return config;
- });
-
- // Add a response interceptor
- http.interceptors.response.use(
- // Return the data if the request is successful
- (res) => res.data,
- // Handle the error if the request is unsuccessful
- (error) => {
- throw error.response?.data || error;
- },
- );
-
- return http;
-}
diff --git a/src/layouts/admin/index.tsx b/src/layouts/admin/index.tsx
index f526845..1839b36 100644
--- a/src/layouts/admin/index.tsx
+++ b/src/layouts/admin/index.tsx
@@ -1,4 +1,4 @@
-import FirstLoader from "@/components/first-loader";
+import FirstLoader from "@/components/FirstLoader";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";
diff --git a/src/layouts/default/index.tsx b/src/layouts/default/index.tsx
index 0d07f4b..36e4b40 100644
--- a/src/layouts/default/index.tsx
+++ b/src/layouts/default/index.tsx
@@ -1,5 +1,5 @@
-import FirstLoader from "@/components/first-loader";
-import useAppSelector from "@/hooks/useAppSelector";
+import FirstLoader from "@/components/FirstLoader";
+import { useAppSelector } from "@/hooks";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";
diff --git a/src/layouts/user/index.tsx b/src/layouts/user/index.tsx
index 37f3148..519b36b 100644
--- a/src/layouts/user/index.tsx
+++ b/src/layouts/user/index.tsx
@@ -1,4 +1,4 @@
-import FirstLoader from "@/components/first-loader";
+import FirstLoader from "@/components/FirstLoader";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";
diff --git a/src/libs/i18n.ts b/src/libs/i18n.ts
index f30f29d..fe96128 100644
--- a/src/libs/i18n.ts
+++ b/src/libs/i18n.ts
@@ -1,25 +1,17 @@
import i18n from "i18next";
import BrowserLanguageDetector from "i18next-browser-languagedetector";
-import ChainedBackend from "i18next-chained-backend";
-import HttpBackend from "i18next-http-backend";
import LocalStorageBackend from "i18next-localstorage-backend";
-import { initReactI18next } from "react-i18next";
-import { i18nextPlugin } from "translation-check";
import resources from "virtual:i18next-loader";
-i18n.use(BrowserLanguageDetector)
- .use(ChainedBackend)
- .use(i18nextPlugin)
- .use(initReactI18next)
- .init({
- backend: {
- backends: [HttpBackend, LocalStorageBackend],
- },
- fallbackLng: "en",
- interpolation: {
- escapeValue: false,
- },
- resources,
- });
+i18n.use(BrowserLanguageDetector).init({
+ backend: {
+ backends: [LocalStorageBackend],
+ },
+ fallbackLng: "en",
+ interpolation: {
+ escapeValue: false,
+ },
+ resources,
+});
export default i18n;
diff --git a/src/models/User.ts b/src/models/User.ts
deleted file mode 100644
index 46f55ef..0000000
--- a/src/models/User.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Base } from "./Base";
-
-/**
- * User model, extended from the base model
- */
-export type User = Base & {
- /**
- * Email of the user
- */
- email: string;
-};
diff --git a/src/services/base.service.ts b/src/services/base.service.ts
index 21a1f01..cd056de 100644
--- a/src/services/base.service.ts
+++ b/src/services/base.service.ts
@@ -1,13 +1,39 @@
-import { AxiosInstance } from "axios";
+import axios, { AxiosInstance } from "axios";
+import { TokenService } from "./token.service";
/**
* Base service class to be extended by other services
* @author Kim Minh Thang
*/
export class BaseService {
+ protected client: AxiosInstance;
+ private readonly tokenService: TokenService;
/**
* Constructor for the base service
- * @param http AxiosInstance to be used for making requests
*/
- constructor(protected readonly http: AxiosInstance) {}
+ constructor(resource: string) {
+ this.tokenService = new TokenService();
+ this.client = axios.create({
+ baseURL: `/api/${resource}`,
+ });
+
+ // Add a request interceptor
+ this.client.interceptors.request.use((config) => {
+ // Get the access token from the token service
+ const accessToken = this.tokenService.accessToken;
+ // If the access token exists, add it to the Authorization header
+ if (accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
+ return config;
+ });
+
+ // Add a response interceptor
+ this.client.interceptors.response.use(
+ // Return the data if the request is successful
+ (res) => res.data,
+ // Handle the error if the request is unsuccessful
+ (error) => {
+ throw error.response?.data || error;
+ },
+ );
+ }
}
diff --git a/src/services/crud.service.ts b/src/services/crud.service.ts
index eb9c287..41ef019 100644
--- a/src/services/crud.service.ts
+++ b/src/services/crud.service.ts
@@ -1,4 +1,4 @@
-import http from "@/http/http";
+import { Base, BaseInput, BaseUpdate } from "@/types/base";
import { BaseService } from "./base.service";
/**
@@ -9,31 +9,22 @@ import { BaseService } from "./base.service";
* - DELETE: Delete
* @author Kim Minh Thang
*/
-export class CrudService extends BaseService {
- /**
- * Constructor for the CRUD service
- * @param resource Resource path for the service
- * @example new CrudService("users") => http://localhost:3000/users
- */
- constructor(resource: string) {
- super(http(resource));
- }
-
+export class CrudService extends BaseService {
/**
* Create a new data in the resource
* @param data - Data to be created
* @returns Created data
*/
- async create(data: Omit): Promise {
- return await this.http.post("", data);
+ create(data: BaseInput): Promise {
+ return this.client.post("", data);
}
/**
* Get all data from the resource
* @returns All data
*/
- async getAll(): Promise {
- return await this.http.get("");
+ getAll(): Promise {
+ return this.client.get("");
}
/**
@@ -41,8 +32,8 @@ export class CrudService extends BaseService {
* @param id ID of the data to be fetched
* @returns Data with the given ID
*/
- async get(id: string): Promise {
- return await this.http.get(id);
+ get(id: string): Promise {
+ return this.client.get(id);
}
/**
@@ -51,15 +42,15 @@ export class CrudService extends BaseService {
* @param data Data to be updated
* @returns Updated data
*/
- async update(id: string, data: Partial): Promise {
- return await this.http.put(id, data);
+ update(id: string, data: BaseUpdate): Promise {
+ return this.client.put(id, data);
}
/**
* Delete a data from the resource
* @param id ID of the data to be deleted
*/
- async delete(id: string): Promise {
- await this.http.delete(id);
+ delete(id: string): Promise {
+ return this.client.delete(id);
}
}
diff --git a/src/services/index.ts b/src/services/index.ts
new file mode 100644
index 0000000..7bbaebb
--- /dev/null
+++ b/src/services/index.ts
@@ -0,0 +1,3 @@
+export * from "./base.service";
+export * from "./crud.service";
+export * from "./token.service";
diff --git a/src/services/token.service.ts b/src/services/token.service.ts
index 451eef6..32b7ab0 100644
--- a/src/services/token.service.ts
+++ b/src/services/token.service.ts
@@ -43,5 +43,3 @@ export class TokenService {
if (typeof window !== "undefined") localStorage.removeItem(this._accessTokenName); // Remove the access token from the local storage
}
}
-
-export default new TokenService() as TokenService;
diff --git a/src/store/user.ts b/src/store/user.ts
index 61a3aad..0f07cf5 100644
--- a/src/store/user.ts
+++ b/src/store/user.ts
@@ -1,4 +1,4 @@
-import { User } from "@/models/User";
+import { User } from "@/types/user";
import { createSlice } from "@reduxjs/toolkit";
/**
diff --git a/src/models/Base.ts b/src/types/base.ts
similarity index 50%
rename from src/models/Base.ts
rename to src/types/base.ts
index 255e2d9..ce8076c 100644
--- a/src/models/Base.ts
+++ b/src/types/base.ts
@@ -15,3 +15,12 @@ export type Base = {
*/
updatedAt: string; // ISO 8601 date string
};
+
+/**
+ * Base input model, used to create a new base
+ * @template T The type of the base
+ * @extends Base
+ */
+export type BaseInput = Omit;
+
+export type BaseUpdate = Partial>;
diff --git a/src/types/user.ts b/src/types/user.ts
new file mode 100644
index 0000000..1fd4094
--- /dev/null
+++ b/src/types/user.ts
@@ -0,0 +1,16 @@
+import { Base, BaseInput } from "./base";
+
+/**
+ * User model, extended from the base model
+ */
+export type User = Base & {
+ /**
+ * Email of the user
+ */
+ email: string;
+};
+
+/**
+ * User input model, used to create a new user
+ */
+export type UserInput = BaseInput;