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;
};