= (props) => {
+ const [isOpen, setIsOpen] = createSignal(false);
+
+ const anchorName = `--anchor-${createUniqueId()}`;
+
+ const toggle = (e: MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsOpen(!isOpen());
+ };
+
+ const close = () => setIsOpen(false);
+
+ return (
+ <>
+
+ {props.trigger({ toggle, isOpen: isOpen() })}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Dropdown;
diff --git a/Frontend/src/components/Federated/GoogleBtn.module.scss b/Frontend/src/components/Federated/GoogleBtn.module.scss
new file mode 100644
index 0000000..fa69ec8
--- /dev/null
+++ b/Frontend/src/components/Federated/GoogleBtn.module.scss
@@ -0,0 +1,114 @@
+/* Path: Frontend/src/components/Federated/GoogleBtn.module.scss */
+
+.gsi-material-button {
+ font-family: inherit;
+
+ border-radius: 5px;
+
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100%;
+ min-width: min-content;
+
+ height: 4rem;
+ padding: 0 1.5rem;
+
+ cursor: pointer;
+ letter-spacing: 0.25px;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ position: relative;
+ outline: none;
+ overflow: hidden;
+
+ transition:
+ background-color 0.218s,
+ border-color 0.218s,
+ box-shadow 0.218s;
+
+ background-color: white;
+ border: 1px solid var(--gray-400);
+ color: #1f1f1f;
+
+ :global([data-color-scheme="dark"]) & {
+ background-color: var(--bg-2);
+ border-color: var(--gray-600);
+ color: var(--text);
+ }
+
+ .gsi-material-button-icon {
+ height: 20px;
+ width: 20px;
+ min-width: 20px;
+ margin-right: 12px;
+ }
+
+ .gsi-material-button-content-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
+ }
+
+ .gsi-material-button-contents {
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .gsi-material-button-state {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ transition: opacity 0.218s;
+ }
+
+ &:not(:disabled):hover,
+ &:not(:disabled):focus {
+ box-shadow:
+ 0 1px 2px 0 rgba(60, 64, 67, 0.3),
+ 0 1px 3px 1px rgba(60, 64, 67, 0.15);
+
+ .gsi-material-button-state {
+ background-color: #303030;
+ opacity: 8%;
+
+ :global([data-color-scheme="dark"]) & {
+ background-color: white;
+ opacity: 8%;
+ }
+ }
+ }
+
+ &:not(:disabled):active .gsi-material-button-state {
+ background-color: #303030;
+ opacity: 12%;
+
+ :global([data-color-scheme="dark"]) & {
+ background-color: white;
+ opacity: 12%;
+ }
+ }
+
+ &:disabled {
+ cursor: default;
+ background-color: #ffffff61;
+ border-color: #1f1f1f1f;
+ box-shadow: none;
+
+ .gsi-material-button-contents,
+ .gsi-material-button-icon {
+ opacity: 38%;
+ }
+
+ :global([data-color-scheme="dark"]) & {
+ background-color: #13131461;
+ border-color: #8e918f1f;
+ }
+ }
+}
diff --git a/Frontend/src/components/Federated/GoogleBtn.tsx b/Frontend/src/components/Federated/GoogleBtn.tsx
new file mode 100644
index 0000000..0b69093
--- /dev/null
+++ b/Frontend/src/components/Federated/GoogleBtn.tsx
@@ -0,0 +1,32 @@
+// Path: Frontend/src/components/Federated/GoogleBtn.tsx
+
+import type { Component, ComponentProps } from "solid-js";
+import { useI18n } from "~/i18n/context";
+import styles from "./GoogleBtn.module.scss";
+
+type GoogleBtnProps = ComponentProps<"button">;
+
+const GoogleBtn: Component = (props) => {
+ const { t } = useI18n();
+
+ return (
+
+ );
+};
+
+export default GoogleBtn;
diff --git a/Frontend/src/components/Navbar/ColorSchemeToggle.module.scss b/Frontend/src/components/Navbar/ColorSchemeToggle.module.scss
new file mode 100644
index 0000000..f7c91d8
--- /dev/null
+++ b/Frontend/src/components/Navbar/ColorSchemeToggle.module.scss
@@ -0,0 +1,59 @@
+/* Path: Frontend/src/components/Navbar/ColorSchemeToggle.smodule.scss */
+
+.toggle-btn {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ width: 3.5rem;
+ height: 3.5rem;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ flex-shrink: 0;
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ color: inherit;
+ transition: background-color 500ms;
+
+ &:hover {
+ background-color: rgba(128, 128, 128, 0.1);
+ }
+}
+
+.icon-container {
+ position: relative;
+ width: 2rem;
+ height: 2rem;
+}
+
+.color-icon {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition:
+ transform 1000ms ease,
+ opacity 500ms;
+}
+
+.moon-wrapper {
+ transform: rotate(90deg);
+ opacity: 0;
+
+ :global([data-color-scheme="dark"]) & {
+ transform: rotate(0deg);
+ opacity: 1;
+ }
+}
+
+.sun-wrapper {
+ transform: rotate(0deg);
+ opacity: 1;
+
+ :global([data-color-scheme="dark"]) & {
+ transform: rotate(-90deg);
+ opacity: 0;
+ }
+}
diff --git a/Frontend/src/components/Navbar/ColorSchemeToggle.tsx b/Frontend/src/components/Navbar/ColorSchemeToggle.tsx
new file mode 100644
index 0000000..d71c73c
--- /dev/null
+++ b/Frontend/src/components/Navbar/ColorSchemeToggle.tsx
@@ -0,0 +1,38 @@
+// Path: Frontend/src/components/Navbar/ColorSchemeToggle.tsx
+
+import type { Component } from "solid-js";
+import Moon from "../SVGs/Moon";
+import Sun from "../SVGs/Sun";
+import styles from "./ColorSchemeToggle.module.scss";
+
+const ColorSchemeToggle: Component = () => {
+ const toggleTheme = () => {
+ const html = document.documentElement;
+ const current = html.getAttribute("data-color-scheme");
+ const next = current === "dark" ? "light" : "dark";
+
+ html.setAttribute("data-color-scheme", next);
+
+ localStorage.setItem("moku-theme", next);
+ };
+
+ return (
+
+ );
+};
+
+export default ColorSchemeToggle;
diff --git a/Frontend/src/components/Navbar/TopRightSimple.module.scss b/Frontend/src/components/Navbar/TopRightSimple.module.scss
new file mode 100644
index 0000000..5632bbd
--- /dev/null
+++ b/Frontend/src/components/Navbar/TopRightSimple.module.scss
@@ -0,0 +1,10 @@
+/* Path: Frontend/src/components/Navbar/TopRightSimple.module.scss */
+
+.topRight {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
diff --git a/Frontend/src/components/Navbar/TopRightSimple.tsx b/Frontend/src/components/Navbar/TopRightSimple.tsx
new file mode 100644
index 0000000..0519c6b
--- /dev/null
+++ b/Frontend/src/components/Navbar/TopRightSimple.tsx
@@ -0,0 +1,18 @@
+// Path: Frontend/src/components/Navbar/TopRightSimple.tsx
+
+import type { Component } from "solid-js";
+import ColorSchemeToggle from "./ColorSchemeToggle";
+import I18nSelector from "./i18nSelector";
+
+import styles from "./TopRightSimple.module.scss";
+
+const TopRightSimple: Component = () => {
+ return (
+
+ );
+};
+
+export default TopRightSimple;
diff --git a/Frontend/src/components/Navbar/i18nSelector.module.scss b/Frontend/src/components/Navbar/i18nSelector.module.scss
new file mode 100644
index 0000000..8ba919d
--- /dev/null
+++ b/Frontend/src/components/Navbar/i18nSelector.module.scss
@@ -0,0 +1,69 @@
+/* Path: Frontend/src/components/Navbar/i18nSelector.module.scss */
+
+.trigger {
+ height: 3.5rem;
+ width: 3.5rem;
+
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ cursor: pointer;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ font-size: 1.5rem;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: var(--bg-2);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
+.option {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+
+ width: 100%;
+
+ padding: 0.6rem 0.8rem;
+
+ background: none;
+ border: none;
+ border-radius: 6px;
+
+ cursor: pointer;
+ text-align: left;
+ font-family: inherit;
+ color: var(--text);
+ transition: background-color 0.15s ease;
+
+ &:hover {
+ background-color: var(--bg-2);
+ }
+
+ &.active {
+ background-color: var(--bg-2);
+ color: var(--primary);
+ font-weight: 600;
+ position: relative;
+ }
+
+ .flag {
+ font-size: 1.25rem;
+ line-height: 1;
+ }
+
+ .label {
+ @include text-smallest;
+ letter-spacing: 0.03em;
+ padding: 0.25rem 8rem 0.25rem 0rem;
+ }
+}
diff --git a/Frontend/src/components/Navbar/i18nSelector.tsx b/Frontend/src/components/Navbar/i18nSelector.tsx
new file mode 100644
index 0000000..5ca5b40
--- /dev/null
+++ b/Frontend/src/components/Navbar/i18nSelector.tsx
@@ -0,0 +1,48 @@
+// Path: Frontend/src/components/Navbar/i18nSelector.tsx
+
+import { For, type Component } from "solid-js";
+import { Dynamic } from "solid-js/web";
+import type { Locale } from "~/i18n/config";
+import { useI18n } from "~/i18n/context";
+import Dropdown from "../Dropdown";
+import FlagHK from "../SVGs/Flags/FlagHK";
+import FlagUS from "../SVGs/Flags/FlagUS";
+import styles from "./i18nSelector.module.scss";
+
+const LOCALE_FLAGS: Record = {
+ en: FlagUS,
+ zh: FlagHK,
+};
+
+const I18nSelector: Component = () => {
+ const { locale, setLocale } = useI18n();
+
+ return (
+ (
+
+ )}
+ >
+ {/* Added a wrapper div for list styling */}
+
+
+ );
+};
+
+export default I18nSelector;
diff --git a/Frontend/src/components/SVGs/Flags/FlagHK.tsx b/Frontend/src/components/SVGs/Flags/FlagHK.tsx
new file mode 100644
index 0000000..2fc8e34
--- /dev/null
+++ b/Frontend/src/components/SVGs/Flags/FlagHK.tsx
@@ -0,0 +1,24 @@
+// Path: Frontend/src/components/SVGs/Flags/FlagHK.tsx
+
+import { mergeProps, splitProps, type Component, type ComponentProps } from "solid-js";
+
+const FlagHK: Component & { size?: number }> = (props) => {
+ const [local, others] = splitProps(mergeProps({ size: 24 }, props), ["size"]);
+
+ return (
+
+ );
+};
+
+export default FlagHK;
diff --git a/Frontend/src/components/SVGs/Flags/FlagUS.tsx b/Frontend/src/components/SVGs/Flags/FlagUS.tsx
new file mode 100644
index 0000000..2e89b76
--- /dev/null
+++ b/Frontend/src/components/SVGs/Flags/FlagUS.tsx
@@ -0,0 +1,25 @@
+// Path: Frontend/src/components/SVGs/Flags/FlagUS.tsx
+
+import { mergeProps, splitProps, type Component, type ComponentProps } from "solid-js";
+
+const FlagUS: Component & { size?: number }> = (props) => {
+ const [local, others] = splitProps(mergeProps({ size: 24 }, props), ["size"]);
+
+ return (
+
+ );
+};
+
+export default FlagUS;
diff --git a/Frontend/src/components/SVGs/Moon.tsx b/Frontend/src/components/SVGs/Moon.tsx
new file mode 100644
index 0000000..6f0dd4c
--- /dev/null
+++ b/Frontend/src/components/SVGs/Moon.tsx
@@ -0,0 +1,16 @@
+// Path: Frontend/src/components/SVGs/Moon.tsx
+
+const Moon = () => {
+ return (
+
+ );
+};
+
+export default Moon;
diff --git a/Frontend/src/components/SVGs/MouseClick.module.scss b/Frontend/src/components/SVGs/MouseClick.module.scss
new file mode 100644
index 0000000..f227a70
--- /dev/null
+++ b/Frontend/src/components/SVGs/MouseClick.module.scss
@@ -0,0 +1,29 @@
+/* Path: Frontend/src/components/SVGs/MouseClick.module.scss */
+
+.mouse-click-icon {
+ margin-top: 3rem;
+ width: 48px;
+ height: 48px;
+ color: var(--text);
+ opacity: 0.6;
+
+ &.animate {
+ animation: mouse-bounce 2s infinite;
+ }
+}
+
+@keyframes mouse-bounce {
+ 0%,
+ 20%,
+ 50%,
+ 80%,
+ 100% {
+ transform: translateY(0);
+ }
+ 40% {
+ transform: translateY(-15px);
+ }
+ 60% {
+ transform: translateY(-7px);
+ }
+}
diff --git a/Frontend/src/components/SVGs/MouseClick.tsx b/Frontend/src/components/SVGs/MouseClick.tsx
new file mode 100644
index 0000000..853c956
--- /dev/null
+++ b/Frontend/src/components/SVGs/MouseClick.tsx
@@ -0,0 +1,31 @@
+// Path: Frontend/src/components/SVGs/MouseClick.tsx
+
+import { mergeProps, type Component } from "solid-js";
+import styles from "./MouseClick.module.scss";
+
+interface Props {
+ animate?: boolean;
+ class?: string;
+}
+
+const MouseClick: Component = (props) => {
+ const merged = mergeProps({ animate: true }, props);
+
+ return (
+
+ );
+};
+
+export default MouseClick;
diff --git a/Frontend/src/components/SVGs/Sun.tsx b/Frontend/src/components/SVGs/Sun.tsx
new file mode 100644
index 0000000..f614110
--- /dev/null
+++ b/Frontend/src/components/SVGs/Sun.tsx
@@ -0,0 +1,64 @@
+// Path: Frontend/src/components/SVGs/Sun.tsx
+
+const Sun = () => {
+ return (
+
+ );
+};
+
+export default Sun;
diff --git a/Frontend/src/entry-client.tsx b/Frontend/src/entry-client.tsx
new file mode 100644
index 0000000..0ca4e3c
--- /dev/null
+++ b/Frontend/src/entry-client.tsx
@@ -0,0 +1,4 @@
+// @refresh reload
+import { mount, StartClient } from "@solidjs/start/client";
+
+mount(() => , document.getElementById("app")!);
diff --git a/Frontend/src/entry-server.tsx b/Frontend/src/entry-server.tsx
new file mode 100644
index 0000000..401eff8
--- /dev/null
+++ b/Frontend/src/entry-server.tsx
@@ -0,0 +1,21 @@
+// @refresh reload
+import { createHandler, StartServer } from "@solidjs/start/server";
+
+export default createHandler(() => (
+ (
+
+
+
+
+
+ {assets}
+
+
+ {children}
+ {scripts}
+
+
+ )}
+ />
+));
diff --git a/Frontend/src/global.d.ts b/Frontend/src/global.d.ts
new file mode 100644
index 0000000..a11339c
--- /dev/null
+++ b/Frontend/src/global.d.ts
@@ -0,0 +1,3 @@
+// Path: Frontend/src/global.d.ts
+
+///
diff --git a/Frontend/src/helpers/env.ts b/Frontend/src/helpers/env.ts
new file mode 100644
index 0000000..eb87a46
--- /dev/null
+++ b/Frontend/src/helpers/env.ts
@@ -0,0 +1,18 @@
+// Path: Frontend/src/helpers/env.ts
+
+export const getApiBaseUrl = (): string => {
+ if (typeof window === "undefined") {
+ return "";
+ }
+
+ const hostname = window.location.hostname;
+ const isDev = hostname === "localhost" || hostname === "127.0.0.1";
+
+ if (isDev) {
+ return "http://localhost:3000/api";
+ } else {
+ return "https://api.mokuapp.com/api";
+ }
+};
+
+export const apiBaseUrl = getApiBaseUrl;
diff --git a/Frontend/src/i18n/config.ts b/Frontend/src/i18n/config.ts
new file mode 100644
index 0000000..edd2abc
--- /dev/null
+++ b/Frontend/src/i18n/config.ts
@@ -0,0 +1,11 @@
+// Path: Frontend/src/i18n/config.ts
+
+import * as en from "./en";
+import * as zh from "./zh";
+
+export type Locale = "en" | "zh";
+
+export const dictionaries = {
+ en: en.dict,
+ zh: zh.dict,
+};
diff --git a/Frontend/src/i18n/context.tsx b/Frontend/src/i18n/context.tsx
new file mode 100644
index 0000000..f141475
--- /dev/null
+++ b/Frontend/src/i18n/context.tsx
@@ -0,0 +1,51 @@
+// Path: Frontend/src/i18n/context.tsx
+
+import * as i18n from "@solid-primitives/i18n";
+import { createContext, createMemo, createSignal, onMount, useContext, type ParentComponent } from "solid-js";
+import { dictionaries, type Locale } from "./config";
+
+type I18nContextType = {
+ locale: () => Locale;
+ setLocale: (l: Locale) => void;
+ t: i18n.Translator;
+ isReady: () => boolean;
+};
+
+const I18nContext = createContext();
+
+export const I18nProvider: ParentComponent = (props) => {
+ const [locale, setLocaleSignal] = createSignal("en");
+ const [isReady, setIsReady] = createSignal(false);
+
+ const setLocale = (l: Locale) => {
+ setLocaleSignal(l);
+ if (typeof window !== "undefined") {
+ localStorage.setItem("lang", l);
+ }
+ };
+
+ onMount(() => {
+ const stored = localStorage.getItem("lang") as Locale;
+
+ if (stored && dictionaries[stored]) {
+ setLocaleSignal(stored);
+ } else if (typeof navigator !== "undefined") {
+ const browserLang = navigator.language.split("-")[0] as Locale;
+
+ if (dictionaries[browserLang]) {
+ setLocaleSignal(browserLang);
+ }
+ }
+
+ setIsReady(true);
+ });
+
+ const dict = createMemo(() => i18n.flatten(dictionaries[locale()]));
+ const t = i18n.translator(dict);
+
+ return {props.children};
+};
+
+export function useI18n() {
+ return useContext(I18nContext)!;
+}
diff --git a/Frontend/src/i18n/en.ts b/Frontend/src/i18n/en.ts
new file mode 100644
index 0000000..86130d0
--- /dev/null
+++ b/Frontend/src/i18n/en.ts
@@ -0,0 +1,30 @@
+// Path: Frontend/src/i18n/en.ts
+
+export const dict = {
+ languages: {
+ en: "English",
+ zh: "中文",
+ },
+ home: {
+ loginBtn: "Login",
+ description: "AI RLHF Platform. Personalize your data collection",
+ },
+ "404": {
+ message: "Sorry! We couldn't find that page.",
+ backHome: "Back to Home",
+ },
+ auth: {
+ federated: {
+ signInWithGoogle: "Sign in with Google",
+ },
+ login: {
+ title: "Login Page",
+ usernameLabel: "Username:",
+ passwordLabel: "Password:",
+ forgotPassword: "Forgot Password?",
+ noAccount: "Don't Have an Account?",
+ registerLink: "Register",
+ loginBtn: "Login",
+ },
+ },
+};
diff --git a/Frontend/src/i18n/zh.ts b/Frontend/src/i18n/zh.ts
new file mode 100644
index 0000000..fae9e67
--- /dev/null
+++ b/Frontend/src/i18n/zh.ts
@@ -0,0 +1,30 @@
+// Path: Frontend/src/i18n/zh.ts
+
+export const dict = {
+ languages: {
+ en: "English",
+ zh: "中文",
+ },
+ home: {
+ loginBtn: "登入",
+ description: "AI RLHF 开发。满足您的数据收集、模型训练和部署需求。",
+ },
+ "404": {
+ message: "對唔住,搵唔到你想找的頁面。",
+ backHome: "返回首頁",
+ },
+ auth: {
+ federated: {
+ signInWithGoogle: "使用 Google 登入",
+ },
+ login: {
+ title: "登入頁面",
+ usernameLabel: "用戶名:",
+ passwordLabel: "密碼:",
+ forgotPassword: "忘記密碼?",
+ noAccount: "還沒有帳號?",
+ registerLink: "註冊",
+ loginBtn: "登入",
+ },
+ },
+};
diff --git a/Frontend/src/routes/404.module.scss b/Frontend/src/routes/404.module.scss
new file mode 100644
index 0000000..e20e8f7
--- /dev/null
+++ b/Frontend/src/routes/404.module.scss
@@ -0,0 +1,15 @@
+/* Path: Frontend/src/routes/404.module.scss */
+
+.notFound {
+ @include base-container;
+
+ h3 {
+ margin-bottom: 1.5rem;
+ }
+
+ .backHomeBtn {
+ @include button-base;
+ background-color: var(--primary);
+ color: var(--primary-text);
+ }
+}
diff --git a/Frontend/src/routes/[...404].tsx b/Frontend/src/routes/[...404].tsx
new file mode 100644
index 0000000..c29efc1
--- /dev/null
+++ b/Frontend/src/routes/[...404].tsx
@@ -0,0 +1,26 @@
+// Path: Frontend/src/routes/[...404].tsx
+
+import { A } from "@solidjs/router";
+import type { Component } from "solid-js";
+import TopRightSimple from "~/components/Navbar/TopRightSimple";
+import { useI18n } from "~/i18n/context";
+
+import styles from "./404.module.scss";
+
+const NotFound: Component = () => {
+ const { t } = useI18n();
+
+ return (
+ <>
+
+ {t("404").message}
+
+
+
+
+
+ >
+ );
+};
+
+export default NotFound;
diff --git a/Frontend/src/routes/auth/forgot-password/index.tsx b/Frontend/src/routes/auth/forgot-password/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/auth/login/index.tsx b/Frontend/src/routes/auth/login/index.tsx
new file mode 100644
index 0000000..71de459
--- /dev/null
+++ b/Frontend/src/routes/auth/login/index.tsx
@@ -0,0 +1,45 @@
+// Path: Frontend/src/routes/auth/login/index.tsx
+
+import { A } from "@solidjs/router";
+import type { Component } from "solid-js";
+import GoogleBtn from "~/components/Federated/GoogleBtn";
+import TopRightSimple from "~/components/Navbar/TopRightSimple";
+import { useI18n } from "~/i18n/context";
+import styles from "./login.module.scss";
+
+const LoginPage: Component = () => {
+ const { t } = useI18n();
+
+ return (
+ <>
+
+ {t("auth").login.title}
+
+
+ {/* Federated */}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default LoginPage;
diff --git a/Frontend/src/routes/auth/login/login.module.scss b/Frontend/src/routes/auth/login/login.module.scss
new file mode 100644
index 0000000..a47cf46
--- /dev/null
+++ b/Frontend/src/routes/auth/login/login.module.scss
@@ -0,0 +1,132 @@
+/* Path: Frontend/src/routes/auth/login/login.module.scss */
+
+.loginMain {
+ @include base-container;
+
+ h1 {
+ @include text-largest;
+ margin-bottom: 2rem;
+
+ @include respond(tablet) {
+ margin-bottom: 2rem;
+ }
+ }
+
+ form {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-width: 500px;
+ background-color: var(--bg-1);
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: var(--box-shadow);
+ padding-bottom: 3rem;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+
+ @include respond(tablet) {
+ margin: 2rem;
+ }
+
+ label {
+ @include text-smallest;
+ color: var(--text-muted);
+ }
+
+ input {
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--gray-400);
+ border-radius: 4px;
+ font-size: 1rem;
+ transition:
+ border-color var(--transition-speed) var(--transition-ease),
+ background-color var(--transition-speed) var(--transition-ease),
+ color var(--transition-speed) var(--transition-ease);
+ background-color: var(--bg-2);
+ color: var(--text);
+ margin-bottom: 1rem;
+
+ &:focus {
+ border-color: var(--primary);
+ outline: none;
+ }
+ }
+
+ .loginBtn {
+ @include button-base;
+ background-color: var(--primary);
+ color: var(--primary-text);
+ }
+ }
+
+ .links {
+ color: var(--text-muted);
+ @include text-smallest;
+
+ a {
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .federatedLogin {
+ width: 100%;
+ max-width: 500px;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ align-items: center;
+
+ margin-top: 1rem;
+ padding-top: 2rem;
+ border-top: 1px solid var(--gray-300);
+ position: relative;
+
+ &::after {
+ content: "or continue with";
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: var(--bg);
+ padding: 0 1rem;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ @include respond(tablet) {
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: stretch;
+ }
+
+ .federatedBtn {
+ @include button-base;
+ flex: 1;
+
+ background-color: var(--bg-1);
+ color: var(--text);
+ border: 1px solid var(--gray-300);
+ font-weight: 500;
+
+ &:hover {
+ background-color: var(--bg-2);
+ border-color: var(--gray-400);
+ }
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ }
+ }
+}
diff --git a/Frontend/src/routes/auth/signup/index.tsx b/Frontend/src/routes/auth/signup/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard.tsx b/Frontend/src/routes/dashboard.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/chat/[chat_id]/index.tsx b/Frontend/src/routes/dashboard/chat/[chat_id]/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/index.tsx b/Frontend/src/routes/dashboard/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/[model_id]/analytics/index.tsx b/Frontend/src/routes/dashboard/model/[model_id]/analytics/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/[model_id]/members/index.tsx b/Frontend/src/routes/dashboard/model/[model_id]/members/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/[model_id]/settings/index.tsx b/Frontend/src/routes/dashboard/model/[model_id]/settings/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/create/index.tsx b/Frontend/src/routes/dashboard/model/create/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/stages/[stage_id]/fine-tune/index.tsx b/Frontend/src/routes/dashboard/model/stages/[stage_id]/fine-tune/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/stages/[stage_id]/index.tsx b/Frontend/src/routes/dashboard/model/stages/[stage_id]/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/stages/[stage_id]/settings/index.tsx b/Frontend/src/routes/dashboard/model/stages/[stage_id]/settings/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/model/stages/create/index.tsx b/Frontend/src/routes/dashboard/model/stages/create/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/org/[org_id]/analytics/index.tsx b/Frontend/src/routes/dashboard/org/[org_id]/analytics/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/org/[org_id]/members/index.tsx b/Frontend/src/routes/dashboard/org/[org_id]/members/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/org/[org_id]/settings/index.tsx b/Frontend/src/routes/dashboard/org/[org_id]/settings/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/org/create/index.tsx b/Frontend/src/routes/dashboard/org/create/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/dashboard/profile/index.tsx b/Frontend/src/routes/dashboard/profile/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/Frontend/src/routes/index.module.scss b/Frontend/src/routes/index.module.scss
new file mode 100644
index 0000000..eb1b0f0
--- /dev/null
+++ b/Frontend/src/routes/index.module.scss
@@ -0,0 +1,21 @@
+/* Path: Frontend/src/routes/index.module.scss */
+
+.homepage {
+ @include base-container;
+
+ h1 {
+ @include text-large;
+ margin-bottom: 2rem;
+ }
+
+ p {
+ margin-bottom: 1.5rem;
+ text-align: center;
+ }
+
+ .loginBtn {
+ @include button-base;
+ background-color: var(--primary);
+ color: var(--primary-text);
+ }
+}
diff --git a/Frontend/src/routes/index.tsx b/Frontend/src/routes/index.tsx
new file mode 100644
index 0000000..de94b1c
--- /dev/null
+++ b/Frontend/src/routes/index.tsx
@@ -0,0 +1,29 @@
+// Path: Frontend/src/routes/index.tsx
+
+import { A } from "@solidjs/router";
+import type { Component } from "solid-js";
+import { useI18n } from "~/i18n/context";
+
+import TopRightSimple from "~/components/Navbar/TopRightSimple";
+import MouseClick from "~/components/SVGs/MouseClick";
+import styles from "./index.module.scss";
+
+const LandingPage: Component = () => {
+ const { t } = useI18n();
+
+ return (
+ <>
+
+ Moku (木)
+ {t("home").description}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default LandingPage;
diff --git a/Frontend/src/styles/_fonts.scss b/Frontend/src/styles/_fonts.scss
new file mode 100644
index 0000000..c373405
--- /dev/null
+++ b/Frontend/src/styles/_fonts.scss
@@ -0,0 +1,15 @@
+/* Path: Frontend/src/styles/_fonts.scss */
+
+@font-face {
+ font-family: "Geist";
+ src: url("/fonts/Geist.woff2") format("woff2");
+ font-weight: 100 900;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "GeistMono";
+ src: url("/fonts/GeistMono.woff2") format("woff2");
+ font-weight: 100 900;
+ font-display: swap;
+}
diff --git a/Frontend/src/styles/_mixins.scss b/Frontend/src/styles/_mixins.scss
new file mode 100644
index 0000000..283e156
--- /dev/null
+++ b/Frontend/src/styles/_mixins.scss
@@ -0,0 +1,111 @@
+/* Path: Frontend/src/styles/_mixins.scss */
+
+// Breakpoints
+$mobile: 768px;
+$tablet: 1024px;
+$desktop: 1440px;
+
+// Mixin for responsive design
+@mixin respond($breakpoint) {
+ @if $breakpoint == mobile {
+ @media (min-width: $mobile) {
+ @content;
+ }
+ } @else if $breakpoint == tablet {
+ @media (min-width: $tablet) {
+ @content;
+ }
+ } @else if $breakpoint == desktop {
+ @media (min-width: $desktop) {
+ @content;
+ }
+ }
+}
+
+// Mixin Base Container
+@mixin base-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ min-height: 100dvh;
+ width: 100%;
+
+ padding-top: 6.5rem;
+ padding-bottom: 1rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+
+ // 3. Tablet adjustment
+ @include respond(tablet) {
+ padding-top: 6rem;
+ padding-bottom: 2rem;
+ }
+}
+
+// Mixin for responsive text sizes
+@mixin text-smallest {
+ font-size: 1rem;
+ font-size: clamp(1rem, 0.9295774647887324rem + 0.300469483568075vw, 1.2rem);
+ font-weight: 300;
+}
+
+@mixin text-smaller {
+ font-size: 1.1rem;
+ font-size: clamp(1.1rem, 0.959154929577465rem + 0.60093896713615vw, 1.5rem);
+ font-weight: 300;
+}
+
+@mixin text-small {
+ font-size: 1.25rem;
+ font-size: clamp(1.25rem, 0.9859154929577465rem + 1.1267605633802815vw, 2rem);
+ font-weight: 300;
+}
+
+@mixin text-medium {
+ font-size: 1.5rem;
+ font-size: clamp(1.5rem, 1.147887323943662rem + 1.5023474178403755vw, 2.5rem);
+ font-weight: 400;
+}
+
+@mixin text-large {
+ font-size: 1.75rem;
+ font-size: clamp(1.75rem, 1.3098591549295775rem + 1.8779342723004695vw, 3rem);
+ font-weight: 400;
+}
+
+@mixin text-largest {
+ font-size: 2rem;
+ font-size: clamp(2rem, 1.295774647887324rem + 3.004694835680751vw, 4rem);
+ font-weight: 400;
+}
+
+// Buttons
+@mixin button-base {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+
+ padding: 15px 40px;
+
+ border-radius: 5px;
+ box-shadow: var(--box-shadow);
+ border: none;
+ cursor: pointer;
+ text-align: center;
+ text-decoration: none;
+ transition: all 250ms;
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: manipulation;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+
+ &:has(svg) {
+ padding: 10px 20px 10px 15px;
+ }
+}
diff --git a/Frontend/src/styles/_reset.scss b/Frontend/src/styles/_reset.scss
new file mode 100644
index 0000000..0d998e9
--- /dev/null
+++ b/Frontend/src/styles/_reset.scss
@@ -0,0 +1,52 @@
+/* Path: Frontend/src/styles/_reset.scss */
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+* {
+ margin: 0;
+}
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+}
+
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ overflow-wrap: break-word;
+}
+
+#root,
+#__next {
+ isolation: isolate;
+}
diff --git a/Frontend/src/styles/main.scss b/Frontend/src/styles/main.scss
new file mode 100644
index 0000000..fca145b
--- /dev/null
+++ b/Frontend/src/styles/main.scss
@@ -0,0 +1,93 @@
+/* Path: Frontend/src/styles/main.scss */
+
+@use "./reset" as *;
+@use "./fonts" as *;
+@use "transitions";
+
+html {
+ background-color: var(--bg);
+
+ transition:
+ background-color var(--transition-speed) var(--transition-ease),
+ color var(--transition-speed) var(--transition-ease);
+
+ color: var(--text);
+ font-family:
+ "Geist",
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ "Open Sans",
+ "Helvetica Neue",
+ sans-serif;
+
+ overflow-x: hidden;
+ scroll-behavior: smooth;
+}
+
+h1 {
+ @include text-largest;
+}
+
+h2 {
+ @include text-large;
+}
+
+h3 {
+ @include text-medium;
+}
+
+h4 {
+ @include text-small;
+}
+
+h5 {
+ @include text-smaller;
+}
+
+p,
+span,
+button,
+input,
+textarea,
+sub,
+a {
+ @include text-smallest;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+p,
+span,
+a,
+button,
+input,
+textarea {
+ transition: color var(--transition-speed) var(--transition-ease);
+}
+
+#app,
+.app-shell {
+ width: 100%;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+}
diff --git a/Frontend/src/styles/theme.scss b/Frontend/src/styles/theme.scss
new file mode 100644
index 0000000..1cf470e
--- /dev/null
+++ b/Frontend/src/styles/theme.scss
@@ -0,0 +1,85 @@
+/* Path: Frontend/src/styles/theme.scss */
+
+:root {
+ --transition-speed: 0.5s;
+ --transition-ease: ease-in-out;
+
+ --gray-50: hsl(40 25% 85%);
+ --gray-100: hsl(40 23% 80%);
+ --gray-200: hsl(40 20% 77%);
+ --gray-300: hsl(40 20% 70%);
+ --gray-400: hsl(40 10% 50%);
+ --gray-500: hsl(40 5% 40%);
+ --gray-600: hsl(40 5% 30%);
+ --gray-700: hsl(40 5% 25%);
+ --gray-800: hsl(40 5% 20%);
+ --gray-900: hsl(40 5% 15%);
+
+ --brown-300: hsl(27 39.2% 86.5%);
+ --brown-400: hsl(27 39.2% 76.5%);
+ --brown-500: hsl(27 39.2% 66.5%);
+ --brown-600: hsl(27 39.2% 56.5%);
+ --brown-700: hsl(27 39.2% 46.5%);
+ --brown-800: hsl(27 39.2% 36.5%);
+
+ --green-300: hsl(115 43.1% 70%);
+ --green-400: hsl(115 43.1% 70%);
+ --green-500: hsl(115 43.1% 60%);
+ --green-600: hsl(115 43.1% 50%);
+ --green-700: hsl(115 43.1% 40%);
+
+ --red-500: hsl(359 46.6% 50.8%);
+ --red-600: hsl(359 46.6% 40.8%);
+}
+
+:root {
+ // Background and Text Colors
+ --bg: var(--gray-50);
+ --text: var(--gray-800);
+ --text-muted: var(--gray-700);
+
+ // Accent
+ --primary: var(--brown-700);
+ --primary-hover: var(--brown-800);
+ --primary-text: var(--gray-50);
+
+ --secondary: var(--green-500);
+ --secondary-hover: var(--green-600);
+ --warning: var(--red-500);
+ --warning-hover: var(--red-600);
+
+ // Typical Card Button Colors
+ --bg-1: var(--gray-100);
+ --bg-1-hover: var(--gray-200);
+
+ --bg-2: var(--gray-200);
+ --bg-2-hover: var(--gray-300);
+
+ --bg-3: var(--gray-300);
+ --bg-3-hover: var(--gray-400);
+
+ --box-shadow: 0 8px 16px hsl(0 0% 0% / 0.1);
+}
+
+[data-color-scheme="dark"] {
+ // Background and Text Colors
+ --bg: var(--gray-900);
+ --text: var(--gray-50);
+ --text-muted: var(--gray-400);
+
+ // Accent
+ --primary: var(--brown-700);
+ --primary-hover: var(--brown-600);
+ --secondary: var(--green-700);
+ --secondary-hover: var(--green-600);
+
+ // Typical Card Button Colors
+ --bg-1: var(--gray-800);
+ --bg-1-hover: var(--gray-700);
+
+ --bg-2: var(--gray-700);
+ --bg-2-hover: var(--gray-600);
+
+ --bg-3: var(--gray-600);
+ --bg-3-hover: var(--gray-500);
+}
diff --git a/Frontend/src/styles/transitions.scss b/Frontend/src/styles/transitions.scss
new file mode 100644
index 0000000..5154ac6
--- /dev/null
+++ b/Frontend/src/styles/transitions.scss
@@ -0,0 +1,14 @@
+/* Path: Frontend/src/styles/transitions.scss */
+
+.fade-enter-active,
+.fade-exit-active {
+ transition: opacity 0.2s ease; // Remove transform here
+}
+.fade-enter,
+.fade-exit-to {
+ opacity: 0;
+}
+.fade-enter-to,
+.fade-exit {
+ opacity: 1;
+}
diff --git a/Frontend/tsconfig.json b/Frontend/tsconfig.json
new file mode 100644
index 0000000..3a1138c
--- /dev/null
+++ b/Frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "allowJs": true,
+ "strict": true,
+ "noEmit": true,
+ "types": ["vinxi/types/client"],
+ "isolatedModules": true,
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ }
+}
diff --git a/README.md b/README.md
index 9dfeb62..ba6b46e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1 @@
-# Moku
-
+# Moku (木)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..45516f6
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ moku-frontend-dev:
+ container_name: moku-frontend-dev
+ build:
+ context: ./Frontend
+ target: development
+ ports:
+ - ${FRONTEND_PORT}:5432
+ volumes:
+ - ./Frontend:/app
+
+ # Install node_modules if not present
+ command: sh -c "if [ ! -d node_modules ]; then pnpm install; fi && pnpm dev --port 5432 --host"
+ profiles:
+ - dev
+
+ moku-frontend-prod:
+ container_name: moku-frontend-prod
+ build:
+ context: ./Frontend
+ target: production
+ ports:
+ - ${FRONTEND_PORT}:80
+ profiles:
+ - prod