diff --git a/.gitignore b/.gitignore index 36e4935..034111e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bun.lock .env /uploads .idea +dist diff --git a/biome.json b/biome.json index 46ee8c9..f415cc6 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "ignore": [] + "ignore": ["dist"] }, "formatter": { "enabled": true, diff --git a/config/index.ts b/config/index.ts index 8eb0582..ddc9a40 100644 --- a/config/index.ts +++ b/config/index.ts @@ -7,6 +7,7 @@ const environment: Environment = { development: process.env.NODE_ENV === "development" || process.argv.includes("--dev"), fqdn: normalizeFqdn(process.env.FQDN) || "http://localhost:8080", + backendUrl: normalizeFqdn(process.env.BACKEND_URL) || "http://localhost:8080", }; function verifyRequiredVariables(): void { diff --git a/package.json b/package.json index ed17f65..6ee16d4 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,9 @@ "private": true, "type": "module", "scripts": { - "start": "bun src/index.ts", + "start": "bun run build && bun src/serve.ts", "dev": "bun src/index.ts --dev", - "build": "vite build", - "serve": "vite preview", + "build": "bun run src/build.ts", "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", "cleanup": "rm -rf logs node_modules bun.lock" @@ -23,6 +22,7 @@ }, "dependencies": { "@creations.works/logger": "^1.0.3", + "@solidjs/router": "^0.15.3", "solid-js": "^1.9.5" } } diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..a22f0b3 --- /dev/null +++ b/src/build.ts @@ -0,0 +1,33 @@ +import { resolve } from "node:path"; +import { environment, verifyRequiredVariables } from "@config"; +import { logger } from "@creations.works/logger"; +import { build } from "vite"; +import type { PluginOption } from "vite"; +import solidPlugin from "vite-plugin-solid"; +import tsconfigPaths from "vite-tsconfig-paths"; + +verifyRequiredVariables(); + +function injectEnvPlugin(): PluginOption { + return { + name: "inject-env", + transformIndexHtml(html) { + return html + .replace("__FQDN__", environment.fqdn) + .replace("__BACKEND_URL__", environment.backendUrl); + }, + }; +} + +await build({ + root: resolve("src"), + publicDir: resolve("src/public"), + plugins: [solidPlugin(), tsconfigPaths(), injectEnvPlugin()], + build: { + outDir: resolve("dist"), + emptyOutDir: true, + target: "esnext", + }, +}); + +logger.info("Production build complete."); diff --git a/src/index.html b/src/index.html index d62bd90..6965562 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,9 @@ Solid App + + + diff --git a/src/index.ts b/src/index.ts index d1695a2..ac86a7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,27 @@ import { resolve } from "node:path"; import { environment, verifyRequiredVariables } from "@config"; import { logger } from "@creations.works/logger"; -import { createServer } from "vite"; +import { type PluginOption, createServer } from "vite"; import solidPlugin from "vite-plugin-solid"; import tsconfigPaths from "vite-tsconfig-paths"; verifyRequiredVariables(); +function injectEnvPlugin(): PluginOption { + return { + name: "inject-env", + transformIndexHtml(html) { + return html + .replace("__FQDN__", environment.fqdn) + .replace("__BACKEND_URL__", environment.backendUrl); + }, + }; +} + const server = await createServer({ root: resolve("src"), publicDir: resolve("src/public"), - plugins: [solidPlugin(), tsconfigPaths()], + plugins: [solidPlugin(), tsconfigPaths(), injectEnvPlugin()], server: { port: environment.port, host: environment.host, diff --git a/src/lib/char.ts b/src/lib/char.ts index 3d90743..4dba90b 100644 --- a/src/lib/char.ts +++ b/src/lib/char.ts @@ -1,5 +1,6 @@ export function normalizeFqdn(value?: string): string | null { if (!value) return null; - if (!/^https?:\/\//.test(value)) return `https://${value}`; - return value; + const trimmed = value.replace(/\/+$/, ""); + if (!/^https?:\/\//.test(trimmed)) return `https://${trimmed}`; + return trimmed; } diff --git a/src/lib/envClient.ts b/src/lib/envClient.ts new file mode 100644 index 0000000..de055b6 --- /dev/null +++ b/src/lib/envClient.ts @@ -0,0 +1,12 @@ +export function getClientEnv() { + const fqdn = + document.querySelector('meta[name="env-fqdn"]')?.getAttribute("content") || + "http://localhost:8080"; + + const backendUrl = + document + .querySelector('meta[name="env-backend-url"]') + ?.getAttribute("content") || "http://localhost:8080"; + + return { fqdn, backendUrl }; +} diff --git a/src/serve.ts b/src/serve.ts new file mode 100644 index 0000000..fa399ee --- /dev/null +++ b/src/serve.ts @@ -0,0 +1,30 @@ +import { resolve } from "node:path"; +import { environment } from "@config"; +import { logger } from "@creations.works/logger"; +import { file } from "bun"; + +const dist = resolve("dist"); + +Bun.serve({ + port: environment.port, + hostname: environment.host, + async fetch(req) { + const url = new URL(req.url); + let pathname = decodeURIComponent(url.pathname); + if (pathname === "/") pathname = "/index.html"; + + const filePath = resolve(dist + pathname); + const bunFile = file(filePath); + + if (await bunFile.exists()) { + return new Response(bunFile); + } + + const fallback = file(resolve(dist, "index.html")); + return new Response(fallback, { + headers: { "Content-Type": "text/html" }, + }); + }, +}); + +logger.info(`Server running at ${environment.fqdn}`); diff --git a/src/views/App.tsx b/src/views/App.tsx deleted file mode 100644 index 3ae6b36..0000000 --- a/src/views/App.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Component } from "solid-js"; - -import styles from "@views/css/App.module.css"; - -const App: Component = () => { - return ( -
-
-
- ); -}; - -export default App; diff --git a/src/views/app.tsx b/src/views/app.tsx new file mode 100644 index 0000000..b087bb6 --- /dev/null +++ b/src/views/app.tsx @@ -0,0 +1,18 @@ +import { Route } from "@solidjs/router"; +import { about } from "@views/pages/about"; +import { error } from "@views/pages/error"; +import { home } from "@views/pages/home"; +import { login } from "@views/pages/login"; + +import type { Component } from "solid-js"; + +const app: Component = () => ( + <> + + + + + +); + +export { app }; diff --git a/src/views/css/App.module.css b/src/views/css/App.module.css deleted file mode 100644 index d5c3782..0000000 --- a/src/views/css/App.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.App { - text-align: center; -} - -.logo { - animation: logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; -} - -.header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.link { - color: #b318f0; -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/views/css/global.css b/src/views/css/global.css new file mode 100644 index 0000000..0cf0da0 --- /dev/null +++ b/src/views/css/global.css @@ -0,0 +1,26 @@ +:root { + --background: #f4f4f4; + --text: #000; + --input-background: #fff; + --input-border: #ccc; + --button-background: #4f46e5; + --button-hover-background: #4338ca; + --button-text: #fff; +} + +:root[data-theme="dark"] { + --background: #18181b; + --text: #f9fafb; + --input-background: #27272a; + --input-border: #3f3f46; + --button-background: #6366f1; + --button-hover-background: #4f46e5; + --button-text: #fff; +} + +body { + margin: 0; + padding: 0; + background-color: var(--background); + color: var(--text); +} diff --git a/src/views/css/index.css b/src/views/css/index.css deleted file mode 100644 index e068437..0000000 --- a/src/views/css/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} diff --git a/src/views/css/login.module.css b/src/views/css/login.module.css new file mode 100644 index 0000000..564fd8a --- /dev/null +++ b/src/views/css/login.module.css @@ -0,0 +1,45 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + background-color: var(--background); +} + +.title { + font-size: 2rem; + margin-bottom: 1rem; + color: var(--text); +} + +.form { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 300px; +} + +.input { + padding: 0.5rem; + font-size: 1rem; + background-color: var(--input-background); + color: var(--text); + border: 1px solid var(--input-border); + border-radius: 4px; +} + +.button { + padding: 0.5rem; + font-size: 1rem; + background-color: var(--button-background); + color: var(--button-text); + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.button:hover { + background-color: var(--button-hover-background); +} diff --git a/src/views/index.tsx b/src/views/index.tsx index 3e7cf4c..2eaa0b6 100644 --- a/src/views/index.tsx +++ b/src/views/index.tsx @@ -1,12 +1,20 @@ +import "@views/css/global.css"; + +import { Router } from "@solidjs/router"; +import { app as App } from "@views/app"; import { render } from "solid-js/web"; -import "@views/css/index.css"; -import App from "@views/App"; +const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; +document.documentElement.dataset.theme = prefersDark ? "dark" : "light"; const root = document.getElementById("root"); +if (!(root instanceof HTMLElement)) throw new Error("Root element not found"); -if (!(root instanceof HTMLElement)) { - throw new Error("Root element not found"); -} - -render(() => , root); +render( + () => ( + + + + ), + root, +); diff --git a/src/views/pages/about.tsx b/src/views/pages/about.tsx new file mode 100644 index 0000000..c79162d --- /dev/null +++ b/src/views/pages/about.tsx @@ -0,0 +1,12 @@ +import type { Component } from "solid-js"; + +const about: Component = () => { + return ( +
+

About

+

This is the about page.

+
+ ); +}; + +export { about }; diff --git a/src/views/pages/error.tsx b/src/views/pages/error.tsx new file mode 100644 index 0000000..bce87c3 --- /dev/null +++ b/src/views/pages/error.tsx @@ -0,0 +1,12 @@ +import type { Component } from "solid-js"; + +const error: Component = () => { + return ( +
+

404 - Not Found

+

The page you're looking for doesn't exist.

+
+ ); +}; + +export { error }; diff --git a/src/views/pages/home.tsx b/src/views/pages/home.tsx new file mode 100644 index 0000000..fbfa83b --- /dev/null +++ b/src/views/pages/home.tsx @@ -0,0 +1,33 @@ +import { getClientEnv } from "@lib/envClient"; +import { useNavigate } from "@solidjs/router"; +import { onMount } from "solid-js"; + +const home = () => { + const navigate = useNavigate(); + const { backendUrl } = getClientEnv(); + + onMount(async () => { + try { + const res = await fetch(`${backendUrl}/auth/session`, { + credentials: "include", + }); + + if (res.ok) { + const data = await res.json(); + if (data?.valid) { + navigate("/dashboard", { replace: true }); + } else { + navigate("/login", { replace: true }); + } + } else { + navigate("/login", { replace: true }); + } + } catch { + navigate("/login", { replace: true }); + } + }); + + return null; +}; + +export { home }; diff --git a/src/views/pages/login.tsx b/src/views/pages/login.tsx new file mode 100644 index 0000000..512ccb0 --- /dev/null +++ b/src/views/pages/login.tsx @@ -0,0 +1,19 @@ +import styles from "@views/css/login.module.css"; +import type { Component } from "solid-js"; + +const login: Component = () => { + return ( +
+

Login

+
+ + + +
+
+ ); +}; + +export { login }; diff --git a/types/config.d.ts b/types/config.d.ts index 1b60b8e..f6d85c6 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -3,4 +3,5 @@ type Environment = { host: string; development: boolean; fqdn: string; + backendUrl: string; };