├── app ├── static │ ├── .gitkeep │ ├── avatar.png │ └── favicon.ico ├── backend │ ├── api │ │ ├── .gitkeep │ │ ├── file_system.ts │ │ └── password_recovery.ts │ ├── files │ │ ├── .gitkeep │ │ ├── initializations.ts │ │ ├── validations.ts │ │ └── pbkdf2.ts │ └── components │ │ ├── .gitkeep │ │ ├── index.ts │ │ ├── login.ts │ │ └── register.ts └── frontend │ ├── css │ ├── .gitkeep │ └── general.css │ ├── files │ ├── .gitkeep │ └── general.ts │ ├── components │ ├── .gitkeep │ ├── parts │ │ ├── error.tsx │ │ ├── success.tsx │ │ ├── loading.tsx │ │ ├── counter.tsx │ │ ├── avatar_menu.tsx │ │ ├── menu.tsx │ │ └── dashboard_summary.tsx │ ├── dashboard.tsx │ ├── password_recovery.tsx │ ├── login.tsx │ ├── new_recovery_password.tsx │ ├── index.tsx │ └── register.tsx │ └── translations │ └── en │ └── index.json ├── .gitignore ├── .vscode └── settings.json ├── options.json ├── main.ts ├── LICENSE ├── .github └── workflows │ └── deno.yml ├── deno.json └── README.md /app/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/backend/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/backend/files/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/files/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/backend/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deno 2 | deno.exe 3 | ngrok 4 | ngrok.exe 5 | *.sqlite* 6 | node_modules 7 | deno.lock -------------------------------------------------------------------------------- /app/frontend/translations/en/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "My SaaS App {{endExample}}" 3 | } 4 | -------------------------------------------------------------------------------- /app/static/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hviana/faster_react/HEAD/app/static/avatar.png -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hviana/faster_react/HEAD/app/static/favicon.ico -------------------------------------------------------------------------------- /app/backend/files/initializations.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "faster"; 2 | Token.setSecret("a3d2r366wgb3dh6yrwzw99kzx2"); 3 | -------------------------------------------------------------------------------- /app/frontend/css/general.css: -------------------------------------------------------------------------------- 1 | /* You can have multiple CSS files and they are automatically compiled. */ 2 | /* You can organize your files into subdirectories here. */ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.defaultFormatter": "denoland.vscode-deno", 5 | "deno.config": "./deno.json" 6 | } 7 | -------------------------------------------------------------------------------- /options.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": { 3 | "kv": { 4 | "pathOrUrl": "", 5 | "DENO_KV_ACCESS_TOKEN": "" 6 | }, 7 | "dev": true, 8 | "serverless": false, 9 | "title": "SaaS Example" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/frontend/components/parts/error.tsx: -------------------------------------------------------------------------------- 1 | const ErrorMessage = (props: any) => { 2 | const { message } = props; 3 | return ( 4 |
8 | {message} 9 |
10 | ); 11 | }; 12 | export default ErrorMessage; 13 | -------------------------------------------------------------------------------- /app/frontend/components/parts/success.tsx: -------------------------------------------------------------------------------- 1 | const SuccessMessage = (props: any) => { 2 | const { message } = props; 3 | return ( 4 |
8 | {message} 9 |
10 | ); 11 | }; 12 | export default SuccessMessage; 13 | -------------------------------------------------------------------------------- /app/frontend/components/parts/loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = (props: any) => { 2 | const { loading } = props; 3 | if (loading) { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 | Loading... 12 |
13 | ); 14 | } 15 | }; 16 | 17 | export default Loading; 18 | -------------------------------------------------------------------------------- /app/backend/files/validations.ts: -------------------------------------------------------------------------------- 1 | const checkPasswordStrength = (password: string): number => { 2 | let strength: number = 0; 3 | if (password.match(/[a-z]+/)) { 4 | strength += 1; 5 | } 6 | if (password.match(/[A-Z]+/)) { 7 | strength += 1; 8 | } 9 | if (password.match(/[0-9]+/)) { 10 | strength += 1; 11 | } 12 | if (password.match(/[$@#&!]+/)) { 13 | strength += 1; 14 | } 15 | if (password.length < 6) { 16 | strength = 0; 17 | } 18 | return strength; 19 | }; 20 | 21 | const validateEmail = (str: string): boolean => { 22 | const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 23 | return re.test(str); 24 | }; 25 | 26 | export { checkPasswordStrength, validateEmail }; 27 | -------------------------------------------------------------------------------- /app/frontend/files/general.ts: -------------------------------------------------------------------------------- 1 | /* Use only frontend libraries here. 2 | You can organize your files into subdirectories here. 3 | Here the extension .ts and .js is used. 4 | You are free to make as many exports or calls (including asynchronous) as you want here. 5 | Different from frontend/components, the scripts here are not automatically delivered to the client. 6 | They need to be imported by the frontend/components. The intention here is to group common functions/objects for React Functions/Components, such as form field validations. 7 | You can also have frontend/files in common for other frontend/files. 8 | */ 9 | const returnHello = () => { 10 | return "Hello"; 11 | }; 12 | 13 | export { returnHello }; 14 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import Builder from "builder"; 2 | import type { JSONObject } from "@helpers/types.ts"; 3 | import options from "./options.json" with { type: "json" }; 4 | import denoJson from "./deno.json" with { type: "json" }; 5 | 6 | const importFromRoot = async (path: string, alias?: any) => { 7 | if (!alias) { 8 | return await import(`./${path}`); 9 | } else { 10 | return await import(`./${path}`, alias); 11 | } 12 | }; 13 | const builder = new Builder(options, denoJson, importFromRoot); 14 | const server = builder.server; 15 | 16 | addEventListener("unhandledrejection", (event) => { 17 | console.error("🛑 Unhandled Rejection:", event.reason); 18 | event.preventDefault(); 19 | }); 20 | 21 | export { options, server }; 22 | 23 | export default { fetch: server.fetch }; 24 | -------------------------------------------------------------------------------- /app/backend/components/index.ts: -------------------------------------------------------------------------------- 1 | import { type BackendComponent } from "@helpers/backend/types.ts"; 2 | import { type Context, type NextFunc } from "faster"; 3 | 4 | await import("../files/initializations.ts"); 5 | 6 | const indexBackendComponent: BackendComponent = { 7 | before: [ 8 | async (ctx: Context, next: NextFunc) => { 9 | if (ctx.req.method !== "GET") { 10 | throw new Error("The home page only accepts the GET method"); 11 | } 12 | await next(); //Calling await next(); is important to continue the flow of execution (or not, if you want to interrupt). 13 | }, 14 | ], 15 | after: async (props) => { //Add properties to the component here. You can pass data from the backend, like from a database, etc. 16 | props["example"] = "props example"; 17 | }, 18 | }; 19 | 20 | export default indexBackendComponent; 21 | -------------------------------------------------------------------------------- /app/frontend/components/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { route } from "@helpers/frontend/route.ts"; 3 | import ResponsiveMenu from "./parts/menu.tsx"; 4 | import DashboardSummary from "./parts/dashboard_summary.tsx"; 5 | const Dashboard = (props: any) => { 6 | const { user, token } = props; 7 | if (!user || !token) { 8 | route({ path: "/pages/login" })(); 9 | return; 10 | } 11 | return ( 12 |
13 | 14 | 15 | {/* Rest of the dashboard content */} 16 |
17 |

Dashboard

18 |
19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Dashboard; 27 | -------------------------------------------------------------------------------- /app/frontend/components/parts/counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const Counter = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

Counter

9 |
10 | 16 | {count} 17 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Counter; 29 | -------------------------------------------------------------------------------- /app/backend/components/login.ts: -------------------------------------------------------------------------------- 1 | import { type BackendComponent } from "@helpers/backend/types.ts"; 2 | import { Server } from "faster"; 3 | import { pbkdf2Verify } from "../files/pbkdf2.ts"; 4 | import { Token } from "faster"; 5 | import { JSONObject } from "@helpers/types.ts"; 6 | const loginBackendComponent: BackendComponent = { 7 | after: async (props) => { 8 | const { email, password } = props; 9 | 10 | if (email && password) { 11 | const user: JSONObject = (await Server.kv.get(["users", email])).value; 12 | if (!user) { 13 | props.error = "Invalid username or password"; 14 | } 15 | if (!props.error) { 16 | if (await pbkdf2Verify(user.password as string, password as string)) { 17 | props.user = user; 18 | delete props.user["password"]; 19 | props.token = await Token.generate({}); 20 | } else { 21 | props.error = "Invalid username or password"; 22 | } 23 | } 24 | } 25 | }, 26 | }; 27 | 28 | export default loginBackendComponent; 29 | -------------------------------------------------------------------------------- /app/backend/api/file_system.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Context, 3 | download, 4 | type NextFunc, 5 | req, 6 | res, 7 | Server, 8 | Token, 9 | upload, 10 | } from "faster"; 11 | 12 | const filesRoute = async (server: Server) => { 13 | server.post( 14 | "/files/*", // For example: /files/general/myFile.xlsx 15 | Token.middleware, 16 | res("json"), 17 | upload(), // Using default options. No controls. 18 | async (ctx: any, next: any) => { 19 | ctx.res.body = ctx.extra.uploadedFiles; 20 | await next(); 21 | }, 22 | ); 23 | 24 | server.get( 25 | "/files/*", 26 | Token.middleware, 27 | download(), // Using default options. No controls. 28 | ); 29 | server.post( 30 | "/avatars/*", 31 | res("json"), 32 | upload({ 33 | maxSizeBytes: async (ctx: Context) => 100000, //100kb 34 | }), 35 | async (ctx: any, next: any) => { 36 | ctx.res.body = ctx.extra.uploadedFiles; 37 | await next(); 38 | }, 39 | ); 40 | 41 | server.get( 42 | "/avatars/*", 43 | download(), // Using default options. No controls. 44 | ); 45 | }; 46 | export default filesRoute; 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Henrique Emanoel Viana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | deno: ["canary", "rc"] 16 | os: [macOS-latest, windows-latest, ubuntu-latest] 17 | include: 18 | - os: ubuntu-latest 19 | cache_path: ~/.cache/deno/ 20 | - os: macos-latest 21 | cache_path: ~/Library/Caches/deno/ 22 | - os: windows-latest 23 | cache_path: ~\AppData\Local\deno\ 24 | 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Deno 30 | uses: denoland/setup-deno@v2 31 | with: 32 | deno-version: ${{ matrix.deno }} 33 | 34 | - name: Verify formatting 35 | if: startsWith(matrix.os, 'ubuntu') && false 36 | run: deno fmt --check 37 | 38 | - name: Run linter 39 | if: startsWith(matrix.os, 'ubuntu') && false 40 | run: deno lint 41 | 42 | - name: Spell-check 43 | if: startsWith(matrix.os, 'ubuntu') && false 44 | uses: crate-ci/typos@master 45 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@faster/react", 3 | "version": "1.0.0", 4 | "exports": {}, 5 | "compilerOptions": { 6 | "lib": ["dom", "dom.asynciterable", "deno.ns"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "react", 9 | "jsxImportSourceTypes": "npm:@types/react@19.1.1" 10 | }, 11 | "unstable": ["cron", "kv", "bundle"], 12 | "lock": false, 13 | "imports": { 14 | "builder": "https://deno.land/x/faster_react_core@v6.7/builder.ts", 15 | "@helpers/": "https://deno.land/x/faster_react_core@v6.7/helpers/", 16 | "@core": "./main.ts", 17 | "react": "npm:react@19.1.1", 18 | "react/": "npm:react@19.1.1/", 19 | "@types/react": "npm:@types/react@19.1.1", 20 | "@types/react/": "npm:@types/react@19.1.1/", 21 | "@types/react-dom": "npm:@types/react-dom@19.1.1", 22 | "i18next": "https://deno.land/x/i18next/index.js", 23 | "react-dom": "npm:react-dom@19.1.1", 24 | "react-dom/server": "npm:react-dom@19.1.1/server", 25 | "react-dom/client": "npm:react-dom@19.1.1/client", 26 | "react/jsx-runtime": "npm:react@19.1.1/jsx-runtime", 27 | "react/jsx-dev-runtime": "npm:react@19.1.1/jsx-dev-runtime", 28 | "walk": "jsr:@std/fs@1.0.19/walk", 29 | "path": "jsr:@std/path@1.1.2", 30 | "faster": "jsr:@hviana/faster@1.1.2", 31 | "deno_kv_fs": "jsr:@hviana/faster@1.1.2/deno-kv-fs", 32 | "jose": "jsr:@hviana/faster@1.1.2/jose", 33 | "b64": "jsr:@std/encoding@1.0.10/base64" 34 | }, 35 | "tasks": { 36 | "serve": "deno serve --allow-all --unstable-kv --watch-hmr=app/ main.ts" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/backend/api/password_recovery.ts: -------------------------------------------------------------------------------- 1 | import { type Context, type NextFunc, req, res, Server } from "faster"; 2 | import { pbkdf2 } from "../files/pbkdf2.ts"; 3 | const recoveryRoutes = async (server: Server) => { 4 | const oneSecond: number = 1000; 5 | const oneMin: number = oneSecond * 60; 6 | const oneHour: number = oneMin * 60; 7 | const oneDay: number = oneHour * 24; 8 | 9 | server.post( 10 | "/recovery", 11 | res("json"), 12 | async (ctx: any, next: any) => { 13 | const data = await ctx.req.json(); 14 | const recoveryTime = oneHour; 15 | const existingUser = (await Server.kv.get(["users", data.email])).value; 16 | if (existingUser) { 17 | let alreadyRecoverSent = false; 18 | if (existingUser.recoveryTimeStamp) { 19 | if ((Date.now() - existingUser.recoveryTimeStamp) < recoveryTime) { 20 | alreadyRecoverSent = true; 21 | } 22 | } 23 | if (!alreadyRecoverSent) { 24 | const recCode = crypto.randomUUID(); 25 | await Server.kv.set(["recovery_codes", recCode], data.email, { 26 | expireIn: recoveryTime, 27 | }); 28 | existingUser.recoveryTimeStamp = Date.now(); 29 | await Server.kv.set(["users", data.email], existingUser); 30 | //TODO send recCode to email 31 | console.log(`User ${data.email}, recovery code: ${recCode}`); 32 | } 33 | } 34 | ctx.res.body = { 35 | success: `Check your email to receive the password recovery code.`, 36 | }; 37 | await next(); 38 | }, 39 | ); 40 | server.get( 41 | "/recovery/:code", 42 | res("json"), 43 | async (ctx: any, next: any) => { 44 | const existingEmail = 45 | (await Server.kv.get(["recovery_codes", ctx.params.code])).value; 46 | if (!existingEmail) { 47 | ctx.res.body = { error: "The code does not exist or has expired." }; 48 | } else { 49 | const user = (await Server.kv.get(["users", existingEmail])).value; 50 | const random6Digits = Math.floor(100000 + Math.random() * 900000) 51 | .toString(); //random 6 digits 52 | user.password = await pbkdf2(random6Digits); 53 | await Server.kv.set(["users", existingEmail], user); 54 | await Server.kv.delete(["recovery_codes", ctx.params.code]); 55 | ctx.res.body = { 56 | success: 57 | `Your temporary password is ${random6Digits}, change it to a secure password as soon as possible.`, 58 | }; 59 | } 60 | await next(); 61 | }, 62 | ); 63 | }; 64 | export default recoveryRoutes; 65 | -------------------------------------------------------------------------------- /app/backend/files/pbkdf2.ts: -------------------------------------------------------------------------------- 1 | const ITERATIONS = 10_000; 2 | const HASH = "SHA-512"; 3 | const SALT_BYTE_LEN = 16; // 16 bytes = 128 bits 4 | const KEY_BYTE_LEN = 64; // 64 bytes = 512 bits 5 | const DERIVE_BITS = KEY_BYTE_LEN * 8; // in bits 6 | 7 | /** 8 | * Convert an ArrayBuffer (or Uint8Array) to hex. 9 | */ 10 | function toHex(buffer: ArrayBuffer | Uint8Array): string { 11 | const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); 12 | return Array.from(bytes) 13 | .map((b) => b.toString(16).padStart(2, "0")) 14 | .join(""); 15 | } 16 | 17 | /** 18 | * Convert a hex string to a Uint8Array. 19 | */ 20 | function fromHex(hex: string): Uint8Array { 21 | const bytes = hex.match(/.{1,2}/g); 22 | if (!bytes) throw new Error("Invalid hex string"); 23 | return new Uint8Array(bytes.map((b) => parseInt(b, 16))); 24 | } 25 | 26 | /** 27 | * Derive a key buffer from password+salt. 28 | */ 29 | async function derive( 30 | password: string, 31 | salt: Uint8Array, 32 | ): Promise { 33 | const pwUtf8 = new TextEncoder().encode(password); 34 | const baseKey = await crypto.subtle.importKey( 35 | "raw", 36 | pwUtf8, 37 | "PBKDF2", 38 | false, 39 | ["deriveBits"], 40 | ); 41 | return await crypto.subtle.deriveBits( 42 | { 43 | name: "PBKDF2", 44 | hash: HASH, 45 | salt, 46 | iterations: ITERATIONS, 47 | }, 48 | baseKey, 49 | DERIVE_BITS, 50 | ); 51 | } 52 | 53 | /** 54 | * Generate a salted PBKDF2 hash. Returns "saltHex:hashHex". 55 | */ 56 | async function pbkdf2(password: string): Promise { 57 | // 1. Generate random salt 58 | const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTE_LEN)); 59 | // 2. Derive the key bytes 60 | const derived = await derive(password, salt); 61 | // 3. Return combined hex 62 | return `${toHex(salt)}:${toHex(derived)}`; 63 | } 64 | 65 | /** 66 | * Verify a password against a stored "saltHex:hashHex". 67 | */ 68 | async function pbkdf2Verify( 69 | stored: string, 70 | password: string, 71 | ): Promise { 72 | const [saltHex, hashHex] = stored.split(":"); 73 | if (!saltHex || !hashHex) return false; 74 | 75 | const salt = fromHex(saltHex); 76 | const derived = await derive(password, salt); 77 | const derivedHex = toHex(derived); 78 | 79 | // Constant-time compare 80 | if (derivedHex.length !== hashHex.length) return false; 81 | let diff = 0; 82 | for (let i = 0; i < hashHex.length; i++) { 83 | diff |= derivedHex.charCodeAt(i) ^ hashHex.charCodeAt(i); 84 | } 85 | return diff === 0; 86 | } 87 | export { pbkdf2, pbkdf2Verify }; 88 | -------------------------------------------------------------------------------- /app/frontend/components/password_recovery.tsx: -------------------------------------------------------------------------------- 1 | import Loading from "./parts/loading.tsx"; 2 | import { useState } from "react"; 3 | import { getJSON } from "@helpers/frontend/route.ts"; 4 | import SuccessMessage from "./parts/success.tsx"; 5 | 6 | const PasswordRecovery = () => { 7 | const [loading, setLoading] = useState(false); 8 | const [message, setMessage] = useState(""); 9 | 10 | return ( 11 |
12 | 13 |
14 |

15 | Reset Your Password 16 |

17 |
{ 21 | event.preventDefault(); 22 | const data: any = new FormData(event.target as any); 23 | const formObject = Object.fromEntries(data.entries()); 24 | const res: any = await getJSON({ 25 | startLoad: () => setLoading(true), 26 | endLoad: () => setLoading(false), 27 | path: "/recovery", 28 | content: formObject, 29 | }); 30 | setMessage(res.success); 31 | if (res.success) { 32 | globalThis.location.href = "/pages/new_recovery_password"; 33 | } 34 | }} 35 | > 36 |
37 | 40 | 47 |
48 | {loading && Loading({ loading: true })} 49 | {message && 50 | SuccessMessage({ message: message })} 51 | 57 |
58 |

59 | Remembered your password?{" "} 60 | 64 | Sign In 65 | 66 |

67 |
68 |
69 | ); 70 | }; 71 | export default PasswordRecovery; 72 | -------------------------------------------------------------------------------- /app/frontend/components/login.tsx: -------------------------------------------------------------------------------- 1 | import { route } from "@helpers/frontend/route.ts"; 2 | import ErrorMessage from "./parts/error.tsx"; 3 | const Login = (props: any) => { 4 | const { user, token, error } = props; 5 | if (user && token) { 6 | route({ 7 | path: "/pages/dashboard", 8 | content: { user: user, token: token }, 9 | })(); 10 | return; 11 | } 12 | return ( 13 |
14 | 15 |
16 |

17 | Sign In to Your Account 18 |

19 |
20 |
21 | 24 | 31 |
32 |
33 | 36 | 43 |
44 | 52 | {error && ErrorMessage({ message: error })} 53 | 59 |
60 |

61 | Don't have an account?{" "} 62 | 66 | Sign Up 67 | 68 |

69 |
70 |
71 | ); 72 | }; 73 | export default Login; 74 | -------------------------------------------------------------------------------- /app/frontend/components/parts/avatar_menu.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { route } from "@helpers/frontend/route.ts"; 3 | const AvatarMenu = (props: any) => { 4 | const { user, token } = props; 5 | const [dropdownOpen, setDropdownOpen] = useState(false); 6 | return ( 7 |
8 |
9 | 21 |
22 | {/* Dropdown menu */} 23 | {dropdownOpen && ( 24 |
25 | 71 |
72 | )} 73 |
74 | ); 75 | }; 76 | export default AvatarMenu; 77 | -------------------------------------------------------------------------------- /app/frontend/components/new_recovery_password.tsx: -------------------------------------------------------------------------------- 1 | import Loading from "./parts/loading.tsx"; 2 | import { useState } from "react"; 3 | import { getJSON } from "@helpers/frontend/route.ts"; 4 | import SuccessMessage from "./parts/success.tsx"; 5 | import ErrorMessage from "./parts/error.tsx"; 6 | 7 | const NewPasswordRecovery = () => { 8 | const [loading, setLoading] = useState(false); 9 | const [successMessage, setSuccessMessage] = useState(""); 10 | const [errorMessage, setErrorMessage] = useState(""); 11 | 12 | return ( 13 |
14 | 15 |
16 |

17 | Enter your recovery code 18 |

19 |
{ 23 | event.preventDefault(); 24 | const data: any = new FormData(event.target as any); 25 | const formObject = Object.fromEntries(data.entries()); 26 | const res: any = await getJSON({ 27 | startLoad: () => setLoading(true), 28 | endLoad: () => setLoading(false), 29 | path: `/recovery/${formObject.code}`, 30 | }); 31 | if (res.success) { 32 | setSuccessMessage(res.success); 33 | } else if (res.error) { 34 | setErrorMessage(res.error); 35 | } else { 36 | setErrorMessage("Unknown error"); 37 | } 38 | }} 39 | > 40 |
41 | 44 | 51 |
52 | {loading && Loading({ loading: true })} 53 | {successMessage && 54 | SuccessMessage({ message: successMessage })} 55 | {errorMessage && 56 | ErrorMessage({ message: errorMessage })} 57 | 63 |
64 |

65 | Remembered your password?{" "} 66 | 70 | Sign In 71 | 72 |

73 |
74 |
75 | ); 76 | }; 77 | export default NewPasswordRecovery; 78 | -------------------------------------------------------------------------------- /app/backend/components/register.ts: -------------------------------------------------------------------------------- 1 | import { type BackendComponent } from "@helpers/backend/types.ts"; 2 | import { checkPasswordStrength, validateEmail } from "../files/validations.ts"; 3 | import { Context, NextFunc, Server, Token } from "faster"; 4 | import { pbkdf2 } from "../files/pbkdf2.ts"; 5 | const signupBackendComponent: BackendComponent = { 6 | before: [ 7 | async (ctx: Context, next: NextFunc) => { 8 | ctx.req = new Request(ctx.req, { 9 | headers: { 10 | ...Object.fromEntries(ctx.req.headers as any), 11 | "Authorization": `Bearer token ${ctx.url.searchParams.get("token")}`, 12 | }, 13 | }); 14 | await next(); 15 | }, 16 | ], 17 | after: async (props) => { 18 | let { name, email, password, avatarUrl, token, update } = props; 19 | if (update) { 20 | try { 21 | await Token.getPayload(token as string); 22 | } catch (e: any) { 23 | props.error = "Unauthenticated user to update profile"; 24 | } 25 | } 26 | 27 | props.updated = false; 28 | props.uploadedAvatar = avatarUrl || ""; 29 | if (name && email) { 30 | if (!validateEmail(email as string)) { 31 | props.error = "Invalid email"; 32 | } 33 | if (!name) { 34 | props.error = "Fill in the name"; 35 | } 36 | const exists = (await Server.kv.get(["users", email])).value; 37 | if (update) { 38 | if (password) { 39 | props.updated = true; 40 | } 41 | if (name != exists.name) { 42 | props.updated = true; 43 | } 44 | if (email != exists.email) { 45 | props.updated = true; 46 | } 47 | if (avatarUrl != exists.avatarUrl) { 48 | props.updated = true; 49 | } 50 | if (!password) { 51 | password = exists.password; 52 | } else { 53 | if (checkPasswordStrength(password as string) < 1) { 54 | props.error = "Very weak password"; 55 | } 56 | password = await pbkdf2(password as string); 57 | props.password = ""; 58 | } 59 | } else { 60 | if (exists) { 61 | props.error = "User already exists"; 62 | } 63 | if (checkPasswordStrength(password as string) < 1) { 64 | props.error = "Very weak password"; 65 | } 66 | password = await pbkdf2(password as string); 67 | } 68 | if (!props.error) { 69 | try { 70 | await Server.kv.set(["users", email], { 71 | name: name, 72 | email: email, 73 | password: password, 74 | avatarUrl: avatarUrl, 75 | }); 76 | } catch (e: any) { 77 | props.error = e.message; 78 | } 79 | 80 | // Simulate user registration logic 81 | // In a real application, save the user to a database 82 | if (!props.error) { 83 | if (props.updated) { 84 | props.message = "Updated successfully, log in again to view."; 85 | } else { 86 | props.message = 87 | `Thank you for signing up, ${name}! Please check your email (${email}) to verify your account.`; 88 | } 89 | } else { 90 | props.message = ""; 91 | } 92 | } 93 | } 94 | }, 95 | }; 96 | 97 | export default signupBackendComponent; 98 | -------------------------------------------------------------------------------- /app/frontend/components/parts/menu.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import AvatarMenu from "./avatar_menu.tsx"; 3 | const ResponsiveMenu = (props: any) => { 4 | const { user, token } = props; 5 | const [isOpen, setIsOpen] = useState(false); 6 | 7 | // Toggle the mobile menu 8 | const toggleMenu = () => { 9 | setIsOpen(!isOpen); 10 | }; 11 | 12 | // Close the mobile menu when a link is clicked 13 | const handleLinkClick = () => { 14 | if (isOpen) { 15 | setIsOpen(false); 16 | } 17 | }; 18 | 19 | return ( 20 | 146 | ); 147 | }; 148 | 149 | export default ResponsiveMenu; 150 | -------------------------------------------------------------------------------- /app/frontend/components/parts/dashboard_summary.tsx: -------------------------------------------------------------------------------- 1 | const DashboardSummary = (props: any) => { 2 | const { data } = props; 3 | return ( 4 | <> 5 | {/* Statistic Cards */} 6 |
7 | {/* Card 1 */} 8 |
9 |
10 |
11 | 16 | 17 | 22 | 23 |
24 |
25 |

26 | 1,257 27 |

28 |
New Users
29 |
30 |
31 |
32 | {/* Card 2 */} 33 |
34 |
35 |
36 | 41 | 42 | 47 | 48 |
49 |
50 |

51 | $24,300 52 |

53 |
Total Sales
54 |
55 |
56 |
57 | {/* Card 3 */} 58 |
59 |
60 |
61 | 66 | 67 | 72 | 73 |
74 |
75 |

152

76 |
Open Tickets
77 |
78 |
79 |
80 | {/* Card 4 */} 81 |
82 |
83 |
84 | 89 | 90 | 95 | 96 |
97 |
98 |

94%

99 |
Customer Satisfaction
100 |
101 |
102 |
103 |
104 | {/* Recent Activities */} 105 |
106 |

107 | Recent Activities 108 |

109 |
110 |
111 | 112 | 113 | 114 | 117 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
115 | User 116 | 118 | Activity 119 | 121 | Time 122 |
John DoeLogged In5 minutes ago
Jane SmithUpdated Profile10 minutes ago
Bob JohnsonMade a Purchase15 minutes ago
143 |
144 |
145 |
146 | {/* Chart Placeholder */} 147 |
148 |

149 | Performance Overview 150 |

151 |
152 |
153 |

[Chart Placeholder]

154 |
155 |
156 |
157 | {/* End of dash-content */} 158 | 159 | ); 160 | }; 161 | export default DashboardSummary; 162 | -------------------------------------------------------------------------------- /app/frontend/components/index.tsx: -------------------------------------------------------------------------------- 1 | import { route } from "@helpers/frontend/route.ts"; 2 | import { 3 | detectedLang, 4 | useTranslation, 5 | } from "@helpers/frontend/translations.ts"; 6 | 7 | const Home = () => { 8 | const t = useTranslation(); 9 | return ( 10 |
11 | 12 | {/* Navigation */} 13 | 33 | 34 | {/* Hero Section */} 35 |
36 |

37 | Welcome to My SaaS App 38 |

39 |

40 | Streamline your workflow and boost productivity with our all-in-one 41 | suite of tools. 42 |

43 | 48 | Get Started 49 | 50 |
51 | 52 | {/* Features Section */} 53 |
54 |
55 |

56 | Powerful Features 57 |

58 |
59 | {/* Feature 1 */} 60 |
61 |
62 |
63 | {/* Light Bulb Icon (Heroicons Outline) */} 64 | 72 | 77 | 78 |
79 |

80 | Intelligent Insights 81 |

82 |

83 | Gain clear and actionable insights into your data, helping you 84 | make smarter decisions, faster. 85 |

86 |
87 |
88 | {/* Feature 2 */} 89 |
90 |
91 |
92 | {/* Check Badge Icon (Heroicons Outline) */} 93 | 101 | 106 | 111 | 112 |
113 |

114 | Reliable Security 115 |

116 |

117 | Safeguard your data with top-notch security features and 118 | industry-leading encryption standards. 119 |

120 |
121 |
122 | {/* Feature 3 */} 123 |
124 |
125 |
126 | {/* Chart Bar Icon (Heroicons Outline) */} 127 | 135 | 140 | 141 |
142 |

143 | Customizable Analytics 144 |

145 |

146 | Tailor analytics dashboards to your needs and watch 147 | productivity soar as you track what matters. 148 |

149 |
150 |
151 |
152 |
153 |
154 | 155 | {/* CTA Section */} 156 |
157 |
158 |

Ready to Get Started?

159 |

160 | Sign up today and transform the way you work. 161 |

162 | 167 | Start Your Free Trial 168 | 169 |
170 |
171 | 172 | {/* Footer */} 173 |
174 |
175 |

176 | © {new Date().getFullYear()} My SaaS App. All rights reserved. 177 |

178 |
179 |
180 |
181 | ); 182 | }; 183 | 184 | export default Home; 185 | -------------------------------------------------------------------------------- /app/frontend/components/register.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { route } from "@helpers/frontend/route.ts"; 3 | 4 | import ErrorMessage from "./parts/error.tsx"; 5 | import SuccessMessage from "./parts/success.tsx"; 6 | import Loading from "./parts/loading.tsx"; 7 | 8 | const Register = (props: any) => { 9 | const { 10 | name, 11 | email, 12 | password, 13 | uploadedAvatar, 14 | error, 15 | message, 16 | update, 17 | updated, 18 | token, 19 | } = props; 20 | const [loading, setLoading] = useState(false); 21 | const [frontendFomError, setFrontendFomError] = useState(""); 22 | const [avatarUrl, setAvatarUrl] = useState(""); 23 | const handleAvatarChange = async (e: React.ChangeEvent) => { 24 | if (e.target.files && e.target.files[0]) { 25 | try { 26 | setLoading(true); 27 | const form = new FormData(); 28 | form.append(e.target.files[0].name, e.target.files[0]); 29 | const req = await fetch(`/avatars/${crypto.randomUUID()}`, { 30 | method: "POST", 31 | body: form, 32 | }); 33 | const res = await req.json(); 34 | const fileKey = Object.keys(res)[0]; 35 | if (res[fileKey].URIComponent) { 36 | setAvatarUrl(`/avatars/${res[fileKey].URIComponent}`); 37 | setFrontendFomError(""); 38 | } else { 39 | setFrontendFomError(res.msg); 40 | } 41 | setLoading(false); 42 | } catch (e) { 43 | setLoading(false); 44 | } 45 | } 46 | }; 47 | if (message && !update) { 48 | return ( 49 |
50 | 51 |
52 |

53 | Registration 54 |

55 |

56 | {message} 57 |

58 | 63 | Go to Login 64 | 65 |
66 |
67 | ); 68 | } else { 69 | return ( 70 |
75 | {!update && } 76 |
77 | {update && ( 78 |

79 | Update Profile 80 |

81 | )} 82 | {!update && ( 83 |

84 | Create Your Account 85 |

86 | )} 87 |
{ 92 | if (!update) { 93 | return; 94 | } else { 95 | event.preventDefault(); 96 | const data: any = new FormData(event.target as any); 97 | const formObject = Object.fromEntries(data.entries()); 98 | await route({ 99 | startLoad: () => setLoading(true), 100 | endLoad: () => setLoading(false), 101 | path: "/components/register", 102 | elSelector: "#dash-content", 103 | content: formObject, 104 | })(); 105 | } 106 | }} 107 | > 108 |
109 | 112 | 120 |
121 |
122 | 125 | 133 |
134 |
135 | 138 | 146 |
147 |
148 | 151 | 152 | 153 | 158 | 167 | {(avatarUrl || uploadedAvatar) && ( 168 | Avatar Preview 173 | )} 174 |
175 | {loading && Loading({ loading: true })} 176 | {(error || frontendFomError) && 177 | ErrorMessage({ message: error || frontendFomError })} 178 | {updated && message && 179 | SuccessMessage({ message: message })} 180 | {update && ( 181 | 187 | )} 188 | {!update && ( 189 | 195 | )} 196 |
197 | {!update && ( 198 |

199 | Already have an account?{" "} 200 | 205 | Sign In 206 | 207 |

208 | )} 209 |
210 |
211 | ); 212 | } 213 | }; 214 | export default Register; 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/badge/development_stage-stable-blue) 2 | ![](https://img.shields.io/badge/tests-pass-green) 3 | 4 | 5 | 6 | # 🚀 **faster_react** 7 | 8 | > [!IMPORTANT]\ 9 | > **Please give a star!** ⭐ 10 | 11 | --- 12 | 13 | ## 🌟 Introduction 14 | 15 | `faster_react` is a tiny Full-Stack React framework. He avoids Overengineering. 16 | This framework **uses its own RSC engine, combining SSR and CSR**, and 17 | automatically generates routes for React components. To utilize this, you must 18 | use the routes helper provided by the framework 19 | ([React Router](#-react-router)). The framework's configuration file is located 20 | at `options.json`. 21 | 22 | ### 🎯 **What Does `faster_react` Do for You?** 23 | 24 | Focus solely on development! This framework handles: 25 | 26 | - 🛣️ **Automatic route generation** for React components. 27 | - 🔄 **Automatic inclusion** of new React components when 28 | `framework => "dev": true`. 29 | - 📦 **Automatic frontend bundling** when `framework => "dev": true`. 30 | - ♻️ **Automatic browser reload** when `framework => "dev": true`. 31 | - 🗜️ **Automatic frontend minification** when `framework => "dev": false`. 32 | - 🚀 **Automatic backend reload** when changes are detected and 33 | `framework => "dev": true`. 34 | - 🌐 **Automatic detection** of Deno Deploy environment. Test in other 35 | serverless environments by setting `framework => "serverless": true`. 36 | 37 | > **Note:** The project includes a simple application example demonstrating each 38 | > functionality. The example uses Tailwind CSS, but this is optional. You can 39 | > use whatever CSS framework you want. 40 | 41 | --- 42 | 43 | ### ⚡ **About Faster** 44 | 45 | This framework uses a middleware library called Faster. Faster is an optimized 46 | middleware server with an incredibly small codebase (~300 lines), built on top 47 | of native HTTP APIs with no dependencies. It includes a collection of useful 48 | middlewares: 49 | 50 | - 📄 **Log file** 51 | - 🗂️ **Serve static** 52 | - 🌐 **CORS** 53 | - 🔐 **Session** 54 | - ⏱️ **Rate limit** 55 | - 🛡️ **Token** 56 | - 📥 **Body parsers** 57 | - 🔀 **Redirect** 58 | - 🔌 **Proxy** 59 | - 📤 **Handle upload** 60 | 61 | Fully compatible with Deno Deploy and other enviroments. Examples of all 62 | resources are available in the [README](https://github.com/hviana/faster). 63 | Faster's ideology is simple: all you need is an optimized middleware manager; 64 | all other functionality is middleware. 65 | 66 | --- 67 | 68 | ## 📚 **Contents** 69 | 70 | - [⚡ Benchmarks](#-benchmarks) 71 | - [🏗️ Architecture](#%EF%B8%8F-architecture) 72 | - [📂 App Structure](#-app-structure) 73 | - [📦 Get Deno Kv and Deno Kv Fs](#-get-deno-kv-and-deno-kv-fs) 74 | - [📝 Backend API](#-backend-api) 75 | - [🧩 Backend Components](#-backend-components) 76 | - [📁 Backend Files](#-backend-files) 77 | - [🖥️ Frontend Components](#%EF%B8%8F-frontend-components) 78 | - [🎨 Frontend CSS](#-frontend-css) 79 | - [📜 Frontend Files](#-frontend-files) 80 | - [🌎 Frontend Translations](#-frontend-translations) 81 | - [🗂️ Static](#%EF%B8%8F-static) 82 | - [🧭 React Router](#-react-router) 83 | - [📦 Packages Included](#-packages-included) 84 | - [🛠️ Creating a Project](#%EF%B8%8F-creating-a-project) 85 | - [🚀 Running a Project](#-running-a-project) 86 | - [🌐 Deploy](#-deploy) 87 | - [📖 References](#-references) 88 | - [👨‍💻 About](#-about) 89 | 90 | --- 91 | 92 | ## ⚡ **Benchmarks** 93 | 94 | `faster_react` has only **0.9%** of the code quantity of Deno Fresh. 95 | 96 | **Benchmark Command:** 97 | 98 | ```bash 99 | # Deno Fresh 100 | git clone https://github.com/denoland/fresh.git 101 | cd fresh 102 | git ls-files | xargs wc -l 103 | # Output: 104132 (version 1.7.3) 104 | 105 | # faster_react 106 | git clone https://github.com/hviana/faster_react.git 107 | cd faster_react 108 | git ls-files | xargs wc -l 109 | # Output: 1037 (version 20.1) 110 | ``` 111 | 112 | --- 113 | 114 | ## 🏗️ **Architecture** 115 | 116 | This framework utilizes **Headless Architecture** [[1]](#1) to build the 117 | application, combined with the **Middleware Design Pattern** [[2]](#2) for 118 | defining API routes in the backend. 119 | 120 | - **Headless Architecture** provides complete freedom to the developer, reducing 121 | the learning curve. Despite this freedom, there is an **explicit separation 122 | between backend and frontend**, which aids in development. 123 | - The **Middleware Design Pattern** offers a practical and straightforward 124 | method for defining API routes. 125 | 126 | ![Architecture Diagram](https://raw.githubusercontent.com/hviana/faster_react_core/refs/heads/main/docs/graph.svg) 127 | 128 | --- 129 | 130 | ## 📂 **App Structure** 131 | 132 | All application folders are inside the `app` folder. 133 | 134 | ### 📦 **Get Deno Kv and Deno Kv Fs** 135 | 136 | On the backend, if a **Deno KV** instance is available, access instances via 137 | `Server.kv` and `Server.kvFs`: 138 | 139 | ```typescript 140 | import { Server } from "faster"; 141 | ``` 142 | 143 | See **Deno KV** settings in `options.json`. 144 | 145 | - **Deno KV File System (`Server.kvFs`):** Compatible with Deno Deploy. Saves 146 | files in 64KB chunks. Organize files into directories, control the KB/s rate 147 | for saving and reading files, impose rate limits, set user space limits, and 148 | limit concurrent operations—useful for controlling uploads/downloads. Utilizes 149 | the Web Streams API. 150 | 151 | More details: [deno_kv_fs](https://github.com/hviana/deno_kv_fs) 152 | 153 | --- 154 | 155 | ### 📝 **Backend API** 156 | 157 | - **Imports:** Import your backend libraries here. 158 | - **Organization:** Files can be organized into subdirectories. 159 | - **File Extension:** Use `.ts` files. 160 | - **Structure:** Flexible file and folder structure that doesn't influence 161 | anything. 162 | - **Routing:** Define routes using any pattern you prefer. 163 | - **Exports:** Must have a `default export` with a function (can be 164 | asynchronous). 165 | - **Function Input:** Receives an instance of `Server` from `faster`. 166 | - **Usage:** Perform backend manipulations here (e.g., fetching data from a 167 | database), including asynchronous calls. 168 | - **Routes:** Define your custom API routes. For help, see: 169 | [faster](https://github.com/hviana/faster) 170 | 171 | --- 172 | 173 | ### 🧩 **Backend Components** 174 | 175 | - **Optionality:** A backend component is optional for a frontend component. 176 | - **Imports:** Import your backend libraries here. 177 | - **Organization:** Organize files into subdirectories. 178 | - **File Extension:** Use `.ts` files. 179 | - **Correspondence:** Each file should have the same folder structure and name 180 | as the corresponding frontend component but with a `.ts` extension. 181 | 182 | - **Example:** 183 | - Frontend: `frontend/components/checkout/cart.tsx` 184 | - Backend: `backend/components/checkout/cart.ts` 185 | 186 | - **Exports:** Must have a `default export` with an object of type 187 | `BackendComponent`: 188 | 189 | ```typescript 190 | import { type BackendComponent } from "@helpers/backend/types.ts"; 191 | ``` 192 | 193 | - **Usage:** Intercept a frontend component request: 194 | - **Before Processing (`before?: RouteFn[]`):** List of middleware functions 195 | (see: [faster](https://github.com/hviana/faster)). Use to check headers 196 | (`ctx.req.headers`) or search params (`ctx.url.searchParams`), like tokens, 197 | impose rate limits etc. 198 | 199 | > **Note:** To cancel page processing, do not call `await next()` at the end 200 | > of a middleware function. 201 | 202 | > **Important:** If you want the page to be processed, **do not** consume 203 | > the `body` of `ctx.req`, or it will cause an error in the framework. 204 | 205 | - **After Processing 206 | (`after?: (props: JSONObject) => void | Promise`):** Function receives 207 | the `props` that will be passed to the component. Add backend data to these 208 | `props`, such as data from a database. Can be asynchronous. 209 | > **Note:** Only use props data in JSON-like representation, or hydration 210 | > will fail. 211 | 212 | --- 213 | 214 | ### 📁 **Backend Files** 215 | 216 | - **Imports:** Import your backend libraries here. 217 | - **Organization:** Organize files into subdirectories. 218 | - **File Extension:** Use `.ts` files. 219 | - **Usage:** Free to make exports or calls (including asynchronous). 220 | - **Purpose:** Group common functions/objects for `backend/api`, 221 | `backend/components`, and other `backend/files`, such as user validations. 222 | 223 | --- 224 | 225 | ### 🖥️ **Frontend Components** 226 | 227 | - **Imports:** Use only frontend libraries. 228 | - **Organization:** Organize files into subdirectories. 229 | - **File Extension:** Use `.tsx` files. 230 | - **Rendering:** Rendered on the server and hydrated on the client. 231 | - **Routes Generated:** Two routes per file (e.g., 232 | `frontend/components/checkout/cart.tsx`): 233 | - **Page Route:** For rendering as a page, e.g., `/pages/checkout/cart`. 234 | - **Component Route:** For rendering as a component, e.g., 235 | `/components/checkout/cart`. 236 | - **Initial Route (`/`):** Points to `frontend/components/index.tsx`. 237 | - **Exports:** Must have a `default export` with the React Function/Component. 238 | - **Props Passed to Component:** 239 | - Form-submitted data (or JSON POST). 240 | - URL search parameters (e.g., `/pages/myPage?a=1&b=2` results in 241 | `{a:1, b:2}`). 242 | - Manipulations from `backend/components`. 243 | 244 | --- 245 | 246 | ### 🎨 **Frontend CSS** 247 | 248 | Application CSS style files. 249 | 250 | - **Multiple Files:** Automatically compiled. 251 | - **Organization:** Organize files into subdirectories. 252 | 253 | --- 254 | 255 | ### 📜 **Frontend Files** 256 | 257 | - **Imports:** Use only frontend libraries. 258 | - **Organization:** Organize files into subdirectories. 259 | - **File Extensions:** Use `.ts` and `.js` files. 260 | - **Usage:** Free to make exports or calls (including asynchronous). 261 | - **Difference from Components:** Scripts are not automatically delivered to the 262 | client. They need to be imported by the `frontend/components`. 263 | - **Purpose:** Group common functions/objects for React Functions/Components, 264 | like form field validations. Can have `frontend/files` common to other 265 | `frontend/files`. 266 | 267 | --- 268 | 269 | ### 🌎 **Frontend Translations** 270 | 271 | - **File Extensions:** Use `.json` files. 272 | - **Correspondence:** Each file should have the same folder structure and name 273 | as the corresponding frontend component but with a `.json` extension. 274 | 275 | - **Example:** 276 | - Frontend: `frontend/components/checkout/cart.tsx` 277 | - Backend: `frontend/translations/en/checkout/cart.json` 278 | > **Note:** Change **en** to your language. 279 | - **Usage:** 280 | 281 | In `frontend/components/index.tsx`: 282 | 283 | ```jsx 284 | import { 285 | detectedLang, 286 | useTranslation, 287 | } from "@helpers/frontend/translations.ts"; 288 | const Home = () => { 289 | const t = useTranslation(); 290 | //Any .init parameter of i18next (minus ns) is valid in useTranslation. 291 | //Ex: useTranslation({ lng: ["es"], fallbackLng: "en" }) etc. 292 | //On the client side, the language is automatically detected (if you don't specify). 293 | //On the server, the language is "en" (if you don't specify). 294 | //The "en" is also the default fallbackLng. 295 | return ( 296 |
297 | {t("index.appName", { endExample: "!" })} 298 |
299 | ); 300 | }; 301 | export default Home; 302 | ``` 303 | 304 | In `frontend/translations/en/index.json`: 305 | 306 | ```json 307 | { 308 | "appName": "My SaaS App {{endExample}}" 309 | } 310 | ``` 311 | 312 | The framework translation is just a wrapper over i18next. See the i18next 313 | documentation if you have questions. 314 | 315 | --- 316 | 317 | ### 🗂️ **Static** 318 | 319 | Files served statically. Routes are generated based on the folder and file 320 | structure. 321 | 322 | - **Example:** `localhost:8080/static/favicon.ico` matches `static/favicon.ico`. 323 | 324 | --- 325 | 326 | ## 🧭 **React Router** 327 | 328 | Since the framework has its own routing system, a third-party routing library is 329 | unnecessary. Use the framework helper: 330 | 331 | > **Note:** Direct form submissions for page routes path also work. 332 | 333 | ```typescript 334 | import { getJSON, route } from "@helpers/frontend/route.ts"; 335 | ``` 336 | 337 | ### **Interface Parameters:** 338 | 339 | ```typescript 340 | interface Route { 341 | headers?: Record; // When routing to a page, headers are encoded in the URL. Intercept them in ctx.url.searchParams in a backend/components file. 342 | content?: 343 | | Record 344 | | (() => Record | Promise>); 345 | path: string; 346 | startLoad?: () => void | Promise; 347 | endLoad?: () => void | Promise; 348 | onError?: (e: Error) => void | Promise; 349 | disableSSR?: boolean; //For component routes. Disables SSR; defaults to false. 350 | elSelector?: string; // Required for component routes. 351 | method?: string; // Only for API routes. Optional; defaults to GET or POST. 352 | useDebounce?: number; //for debounce functionality 353 | } 354 | ``` 355 | 356 | ### **Examples** 357 | 358 | **Navigating to a Page with Search Params:** 359 | 360 | ```jsx 361 | // URL search params passed as properties to the page. Props receive `{a:1}` 362 | ; 365 | ``` 366 | 367 | **Passing Additional Parameters:** 368 | 369 | ```jsx 370 | // Props receive `{a:1, example:"exampleStr"}` 371 | ; 379 | ``` 380 | 381 | **Using Asynchronous Content:** 382 | 383 | ```jsx 384 | // Props receive `{a:1, ...JSONResponse}` 385 | ; 400 | ``` 401 | 402 | **Programmatic Routing:** 403 | 404 | ```typescript 405 | (async () => { 406 | if (user.loggedIn) { 407 | await route({ 408 | path: "/pages/dash", 409 | content: { userId: user.id, token: token }, 410 | })(); 411 | } else { 412 | await route({ path: "/pages/users/login" })(); 413 | } 414 | })(); 415 | ``` 416 | 417 | **Loading a Component:** 418 | 419 | ```jsx 420 | ; 428 | ``` 429 | 430 | **Making an API Call:** 431 | 432 | ```jsx 433 | ; 447 | ``` 448 | 449 | In the case of page routes, you can use this example to pass the URL parameters 450 | for the headers in the backend (if you really need it): 451 | 452 | ```typescript 453 | const signupBackendComponent: BackendComponent = { 454 | before: [ 455 | async (ctx: Context, next: NextFunc) => { 456 | ctx.req = new Request(ctx.req, { 457 | headers: { 458 | ...Object.fromEntries(ctx.req.headers as any), 459 | "Authorization": `Bearer token ${ctx.url.searchParams.get("token")}`, 460 | }, 461 | }); 462 | await next(); 463 | }, 464 | ], 465 | }; 466 | export default signupBackendComponent; 467 | ``` 468 | 469 | Forms submit for page routes work. For components, you can use the following: 470 | 471 | ```tsx 472 |
{ 477 | event.preventDefault(); 478 | const data: any = new FormData(event.target as any); 479 | const formObject = Object.fromEntries(data.entries()); 480 | await route({ 481 | startLoad: () => setLoading(true), //useState 482 | endLoad: () => setLoading(false), 483 | path: "/components/register", 484 | elSelector: "#dash-content", 485 | content: formObject, 486 | })(); 487 | }} 488 | > 489 | ``` 490 | 491 | --- 492 | 493 | ## 📦 **Packages Included** 494 | 495 | Several packages are included to assist in developing React applications. Here 496 | are some examples of imports you can use without additional configuration: 497 | 498 | ```typescript 499 | import {/* your imports */} from "react"; 500 | import {/* your imports */} from "react/"; 501 | import {/* your imports */} from "i18next"; 502 | import {/* your imports */} from "react-dom"; 503 | import {/* your imports */} from "react-dom/server"; 504 | import {/* your imports */} from "react-dom/client"; 505 | import {/* your imports */} from "react/jsx-runtime"; 506 | s; 507 | import {/* your imports */} from "@helpers/frontend/route.ts"; 508 | import {/* your imports */} from "@helpers/frontend/translations.ts"; 509 | import {/* your imports */} from "@helpers/backend/types.ts"; 510 | import {/* your imports */} from "faster"; 511 | import {/* your imports */} from "deno_kv_fs"; 512 | import {/* your imports */} from "jose"; //manage tokens 513 | import { options, server } from "@core"; // Useful for accessing the server instance. 514 | ``` 515 | 516 | --- 517 | 518 | ## 🛠️ **Creating a Project** 519 | 520 | You can simply download this repository. Alternatively, use the command 521 | (requires `git` installed and configured): 522 | 523 | ```bash 524 | deno run -A -r "https://deno.land/x/faster_react_core/new.ts" myProjectFolder 525 | ``` 526 | 527 | Customize and configure the server in `options.json`. 528 | 529 | --- 530 | 531 | ## 🚀 **Running a Project** 532 | 533 | Execute the command: 534 | 535 | Development: 536 | 537 | ```bash 538 | deno task serve 539 | ``` 540 | 541 | Production: 542 | 543 | ```bash 544 | deno serve main.ts #Add your permissions, port, certificate etc. see: https://docs.deno.com/runtime/reference/cli/serve 545 | ``` 546 | 547 | --- 548 | 549 | ## 🌐 **Deploy** 550 | 551 | - **Install Deployctl:** 552 | 553 | ```bash 554 | deno install -A --global jsr:@deno/deployctl 555 | ``` 556 | 557 | - **Deploy Your Project:** 558 | 559 | ```bash 560 | deployctl deploy 561 | ``` 562 | 563 | > **Note:** For production, set `framework => "dev": false` in `options.json`. 564 | 565 | --- 566 | 567 | ## 📖 **References** 568 | 569 | [1] Dragana Markovic, Milic Scekic, Alessio Bucaioni, and Antonio 570 | Cicchetti. 2022. _Could Jamstack Be the Future of Web Applications Architecture? 571 | An Empirical Study._ In _Proceedings of the 37th ACM/SIGAPP Symposium on Applied 572 | Computing_ (SAC '22). Association for Computing Machinery, New York, NY, USA, 573 | 1872–1881. DOI: 574 | [10.1145/3477314.3506991](https://doi.org/10.1145/3477314.3506991) 575 | 576 | [2] Brown, Ethan. _Web Development with Node and Express: 577 | Leveraging the JavaScript Stack_. O'Reilly Media, 2019. URL: 578 | [http://www.oreilly.com/catalog/9781492053484](http://www.oreilly.com/catalog/9781492053484) 579 | 580 | --- 581 | 582 | ## 👨‍💻 **About** 583 | 584 | **Author:** Henrique Emanoel Viana, a Brazilian computer scientist and web 585 | technology enthusiast. 586 | 587 | - 📞 **Phone:** +55 (41) 99999-4664 588 | - 🌐 **Website:** 589 | [https://sites.google.com/view/henriqueviana](https://sites.google.com/view/henriqueviana) 590 | 591 | > **Improvements and suggestions are welcome!** 592 | 593 | --- 594 | --------------------------------------------------------------------------------