├── 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 | setCount(count - 1)}
12 | className="bg-red-500 text-white px-4 py-2 rounded-l hover:bg-red-600 focus:outline-none"
13 | >
14 | –
15 |
16 | {count}
17 | setCount(count + 1)}
19 | className="bg-green-500 text-white px-4 py-2 rounded-r hover:bg-green-600 focus:outline-none"
20 | >
21 | +
22 |
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 |
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 |
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 |
setDropdownOpen(!dropdownOpen)}
14 | >
15 |
20 |
21 |
22 | {/* Dropdown menu */}
23 | {dropdownOpen && (
24 |
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 |
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 |
21 |
22 | {/* Logo */}
23 |
28 |
29 | {/* Desktop Menu */}
30 |
61 |
62 | {/* Mobile Menu Button */}
63 |
64 |
70 | {isOpen
71 | ? (
72 | // Close Icon (X)
73 |
80 |
86 |
87 | )
88 | : (
89 | // Hamburger Icon
90 |
97 |
103 |
104 | )}
105 |
106 |
107 |
108 |
109 | {/* Mobile Menu */}
110 | {isOpen && (
111 |
144 | )}
145 |
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 |
24 |
25 |
26 | 1,257
27 |
28 |
New Users
29 |
30 |
31 |
32 | {/* Card 2 */}
33 |
34 |
35 |
49 |
50 |
51 | $24,300
52 |
53 |
Total Sales
54 |
55 |
56 |
57 | {/* Card 3 */}
58 |
59 |
60 |
74 |
75 |
152
76 |
Open Tickets
77 |
78 |
79 |
80 | {/* Card 4 */}
81 |
82 |
83 |
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 |
115 | User
116 |
117 |
118 | Activity
119 |
120 |
121 | Time
122 |
123 |
124 |
125 |
126 |
127 | John Doe
128 | Logged In
129 | 5 minutes ago
130 |
131 |
132 | Jane Smith
133 | Updated Profile
134 | 10 minutes ago
135 |
136 |
137 | Bob Johnson
138 | Made a Purchase
139 | 15 minutes ago
140 |
141 |
142 |
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 |
14 |
15 | {t("index.appName", { endExample: "!" })}
16 |
17 |
32 |
33 |
34 | {/* Hero Section */}
35 |
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 |
171 |
172 | {/* Footer */}
173 |
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 |
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 |
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 | 
2 | 
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 | 
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 |
363 | Go to Test Page
364 | ;
365 | ```
366 |
367 | **Passing Additional Parameters:**
368 |
369 | ```jsx
370 | // Props receive `{a:1, example:"exampleStr"}`
371 |
377 | Go to Test Page with Extra Data
378 | ;
379 | ```
380 |
381 | **Using Asynchronous Content:**
382 |
383 | ```jsx
384 | // Props receive `{a:1, ...JSONResponse}`
385 | {
389 | return await getJSON({
390 | path: "/example/json",
391 | content: {
392 | test: "testData",
393 | },
394 | });
395 | },
396 | })}
397 | >
398 | Go to Test Page with Async Data
399 | ;
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 |
426 | Load Counter Component
427 | ;
428 | ```
429 |
430 | **Making an API Call:**
431 |
432 | ```jsx
433 | {
435 | const res = await getJSON({
436 | path: "/example/json",
437 | content: {
438 | test: "testData",
439 | },
440 | });
441 | console.log(res);
442 | alert(JSON.stringify(res));
443 | }}
444 | >
445 | Fetch JSON Data
446 | ;
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 |