Day 01
This commit is contained in:
parent
a5ebef7be8
commit
c02811038f
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.output
|
||||||
|
.vinxi
|
||||||
|
.pnpm-store
|
||||||
5
Frontend/.dockerignore
Normal file
5
Frontend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
28
Frontend/Dockerfile
Normal file
28
Frontend/Dockerfile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Base Stage
|
||||||
|
FROM node:24.12.0-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
COPY pnpm-lock.yaml package.json ./
|
||||||
|
|
||||||
|
# Development Stage
|
||||||
|
FROM base AS development
|
||||||
|
EXPOSE 5432
|
||||||
|
CMD ["sh", "-c", "pnpm install && pnpm dev --host"]
|
||||||
|
|
||||||
|
# Build Stage
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# Production Stage
|
||||||
|
FROM fholzer/nginx-brotli:v1.28.0 AS production
|
||||||
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=build /app/.output/public /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
64
Frontend/app.config.ts
Normal file
64
Frontend/app.config.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Path: Frontend/app.config.ts
|
||||||
|
|
||||||
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
preset: "static",
|
||||||
|
prerender: {
|
||||||
|
crawlLinks: true,
|
||||||
|
routes: ["/", "/404", "/auth/login"],
|
||||||
|
failonError: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
vite: ({ router }) => {
|
||||||
|
return {
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
},
|
||||||
|
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `@use "/src/styles/_mixins.scss" as *; \n`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
allowedHosts: ["mangopig.tech", "komorebi-ai.com", "mangopig.com", "mangopiggy.com", "localhost"],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "inline-css-build",
|
||||||
|
apply: "build",
|
||||||
|
enforce: "post",
|
||||||
|
//@ts-ignore
|
||||||
|
transformIndexHtml(html, ctx) {
|
||||||
|
if (!ctx.bundle) return html;
|
||||||
|
//@ts-ignore
|
||||||
|
const cssFile = Object.values(ctx.bundle).find((file) => file.fileName.endsWith(".css"));
|
||||||
|
//@ts-ignore
|
||||||
|
if (!cssFile || !("source" in cssFile)) return html;
|
||||||
|
const styleTag = `<style>${cssFile.source}</style>`;
|
||||||
|
return html.replace(/<link rel="stylesheet"[^>]*?>/, styleTag);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "gzip",
|
||||||
|
ext: ".gz",
|
||||||
|
}),
|
||||||
|
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "brotliCompress",
|
||||||
|
ext: ".br",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
38
Frontend/nginx.conf
Normal file
38
Frontend/nginx.conf
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
brotli_static on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Assets & Fonts
|
||||||
|
location ~* \.(?:css|js|woff2?|png|jpg|ico|svg)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
|
||||||
|
# CSP Report Only
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
add_header Content-Security-Policy-Report-Only "default-src 'self' *.mangopig.tech; script-src 'self' 'unsafe-inline' *.mangopig.tech; style-src 'self' 'unsafe-inline' *.mangopig.tech; img-src 'self' data: *.mangopig.tech; font-src 'self' data: *.mangopig.tech; connect-src 'self'" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 404 Handling
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location = /404.html {
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Frontend/package.json
Normal file
30
Frontend/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "moku-frontend",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vinxi dev",
|
||||||
|
"build": "vinxi build",
|
||||||
|
"start": "vinxi start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solid-primitives/i18n": "^2.2.1",
|
||||||
|
"@solidjs/meta": "^0.29.4",
|
||||||
|
"@solidjs/router": "^0.15.4",
|
||||||
|
"@solidjs/start": "^1.1.0",
|
||||||
|
"solid-js": "^1.9.5",
|
||||||
|
"solid-transition-group": "^0.3.0",
|
||||||
|
"vinxi": "^0.5.7"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"defaults"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cssnano": "^7.1.2",
|
||||||
|
"postcss-preset-env": "^10.6.0",
|
||||||
|
"sass": "^1.97.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6636
Frontend/pnpm-lock.yaml
generated
Normal file
6636
Frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
Frontend/postcss.config.cjs
Normal file
8
Frontend/postcss.config.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require("postcss-preset-env")({
|
||||||
|
stage: 1,
|
||||||
|
}),
|
||||||
|
require("cssnano"),
|
||||||
|
],
|
||||||
|
};
|
||||||
BIN
Frontend/public/favicon/android-chrome-192x192.png
Normal file
BIN
Frontend/public/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
Frontend/public/favicon/android-chrome-512x512.png
Normal file
BIN
Frontend/public/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
Frontend/public/favicon/apple-touch-icon.png
Normal file
BIN
Frontend/public/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
Frontend/public/favicon/favicon-16x16.png
Normal file
BIN
Frontend/public/favicon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 866 B |
BIN
Frontend/public/favicon/favicon-32x32.png
Normal file
BIN
Frontend/public/favicon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Frontend/public/favicon/favicon.ico
Normal file
BIN
Frontend/public/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
Frontend/public/fonts/Geist.woff2
Normal file
BIN
Frontend/public/fonts/Geist.woff2
Normal file
Binary file not shown.
BIN
Frontend/public/fonts/GeistMono.woff2
Normal file
BIN
Frontend/public/fonts/GeistMono.woff2
Normal file
Binary file not shown.
2
Frontend/public/robots.txt
Normal file
2
Frontend/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
79
Frontend/src/app.tsx
Normal file
79
Frontend/src/app.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Path: Frontend/src/app.tsx
|
||||||
|
|
||||||
|
import { Link, Meta, MetaProvider, Title } from "@solidjs/meta";
|
||||||
|
import { Router, useLocation } from "@solidjs/router";
|
||||||
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
|
import { Show, Suspense } from "solid-js";
|
||||||
|
import { I18nProvider, useI18n } from "./i18n/context";
|
||||||
|
|
||||||
|
import { Transition } from "solid-transition-group";
|
||||||
|
import "~/styles/main.scss";
|
||||||
|
import "~/styles/theme.scss";
|
||||||
|
|
||||||
|
const themeScript = `
|
||||||
|
(function() {
|
||||||
|
const getTheme = () => {
|
||||||
|
// 1. Check consistent key "moku-theme"
|
||||||
|
if (typeof localStorage !== "undefined" && localStorage.getItem("moku-theme")) {
|
||||||
|
return localStorage.getItem("moku-theme");
|
||||||
|
}
|
||||||
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
return "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
// 2. Set the ATTRIBUTE, not the class, to match your SCSS
|
||||||
|
document.documentElement.setAttribute("data-color-scheme", theme);
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AppContent = (props: { children: any }) => {
|
||||||
|
const { isReady } = useI18n();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={isReady()} fallback={<div class="h-screen w-screen bg-white dark:bg-black" />}>
|
||||||
|
<Transition name="fade" mode="outin">
|
||||||
|
<Show when={location.pathname} keyed>
|
||||||
|
<div class="app-shell">
|
||||||
|
<Suspense fallback={<div />}>{props.children}</Suspense>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Transition>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<Router
|
||||||
|
root={(props) => (
|
||||||
|
<MetaProvider>
|
||||||
|
<Title>Moku Dashboard</Title>
|
||||||
|
<Meta charset="utf-8" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
{/* Favicon Links */}
|
||||||
|
<Link rel="icon" href="/favicon/favicon.ico" />
|
||||||
|
<Link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
||||||
|
<Link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||||
|
<Link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||||
|
|
||||||
|
{/* Theme Script */}
|
||||||
|
<script innerHTML={themeScript} />
|
||||||
|
|
||||||
|
{/* Preload Fonts */}
|
||||||
|
<Link rel="preload" href="/fonts/Geist.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||||
|
|
||||||
|
<I18nProvider>
|
||||||
|
<AppContent>{props.children}</AppContent>
|
||||||
|
</I18nProvider>
|
||||||
|
</MetaProvider>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileRoutes />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
Frontend/src/components/Dropdown.module.scss
Normal file
67
Frontend/src/components/Dropdown.module.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/* Path: Frontend/src/components/Dropdown.module.scss */
|
||||||
|
|
||||||
|
.triggerWrapper {
|
||||||
|
display: inline-block;
|
||||||
|
anchor-name: var(--anchor-name);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
background: transparent;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
z-index: 9999;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--bg-3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
animation: popIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
|
@supports (anchor-name: --foo) {
|
||||||
|
position-anchor: var(--anchor-target);
|
||||||
|
top: anchor(bottom);
|
||||||
|
transform: none;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
right: anchor(right);
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
left: anchor(left);
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
position-try-options: flip-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes popIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Frontend/src/components/Dropdown.tsx
Normal file
45
Frontend/src/components/Dropdown.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Path: Frontend/src/components/Dropdown.tsx
|
||||||
|
|
||||||
|
import { createSignal, createUniqueId, Show, type JSX, type ParentComponent } from "solid-js";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
import styles from "./Dropdown.module.scss";
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
trigger: (props: { toggle: (e: MouseEvent) => void; isOpen: boolean }) => JSX.Element;
|
||||||
|
children: JSX.Element;
|
||||||
|
align?: "left" | "right";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropdown: ParentComponent<DropdownProps> = (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 (
|
||||||
|
<>
|
||||||
|
<div class={styles.triggerWrapper} style={{ "--anchor-name": anchorName }}>
|
||||||
|
{props.trigger({ toggle, isOpen: isOpen() })}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isOpen()}>
|
||||||
|
<Portal>
|
||||||
|
<div class={styles.backdrop} onClick={close} />
|
||||||
|
|
||||||
|
<div class={`${styles.menu} ${props.align === "left" ? styles.left : styles.right}`} style={{ "--anchor-target": anchorName }} onClick={close}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
114
Frontend/src/components/Federated/GoogleBtn.module.scss
Normal file
114
Frontend/src/components/Federated/GoogleBtn.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Frontend/src/components/Federated/GoogleBtn.tsx
Normal file
32
Frontend/src/components/Federated/GoogleBtn.tsx
Normal file
@ -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<GoogleBtnProps> = (props) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button {...props} class={`${styles["gsi-material-button"]} ${props.class || ""}`}>
|
||||||
|
<div class={styles["gsi-material-button-state"]}></div>
|
||||||
|
<div class={styles["gsi-material-button-content-wrapper"]}>
|
||||||
|
<div class={styles["gsi-material-button-icon"]}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style={{ display: "block" }}>
|
||||||
|
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
|
||||||
|
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
|
||||||
|
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
|
||||||
|
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
|
||||||
|
<path fill="none" d="M0 0h48v48H0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class={styles["gsi-material-button-contents"]}>{t("auth").federated.signInWithGoogle}</span>
|
||||||
|
<span style={{ display: "none" }}>{t("auth").federated.signInWithGoogle}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GoogleBtn;
|
||||||
59
Frontend/src/components/Navbar/ColorSchemeToggle.module.scss
Normal file
59
Frontend/src/components/Navbar/ColorSchemeToggle.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Frontend/src/components/Navbar/ColorSchemeToggle.tsx
Normal file
38
Frontend/src/components/Navbar/ColorSchemeToggle.tsx
Normal file
@ -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 (
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
aria-label="Toggle Dark Mode"
|
||||||
|
class={styles["toggle-btn"]}
|
||||||
|
onClick={toggleTheme} // Direct event binding
|
||||||
|
>
|
||||||
|
<div class={styles["icon-container"]}>
|
||||||
|
<span class={`${styles["color-icon"]} ${styles["moon-wrapper"]}`}>
|
||||||
|
<Moon />
|
||||||
|
</span>
|
||||||
|
<span class={`${styles["color-icon"]} ${styles["sun-wrapper"]}`}>
|
||||||
|
<Sun />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorSchemeToggle;
|
||||||
10
Frontend/src/components/Navbar/TopRightSimple.module.scss
Normal file
10
Frontend/src/components/Navbar/TopRightSimple.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
18
Frontend/src/components/Navbar/TopRightSimple.tsx
Normal file
18
Frontend/src/components/Navbar/TopRightSimple.tsx
Normal file
@ -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 (
|
||||||
|
<nav class={styles.topRight}>
|
||||||
|
<I18nSelector />
|
||||||
|
<ColorSchemeToggle />
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopRightSimple;
|
||||||
69
Frontend/src/components/Navbar/i18nSelector.module.scss
Normal file
69
Frontend/src/components/Navbar/i18nSelector.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Frontend/src/components/Navbar/i18nSelector.tsx
Normal file
48
Frontend/src/components/Navbar/i18nSelector.tsx
Normal file
@ -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<string, Component> = {
|
||||||
|
en: FlagUS,
|
||||||
|
zh: FlagHK,
|
||||||
|
};
|
||||||
|
|
||||||
|
const I18nSelector: Component = () => {
|
||||||
|
const { locale, setLocale } = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
align="right"
|
||||||
|
trigger={({ toggle }) => (
|
||||||
|
<button class={styles.trigger} onClick={toggle} title="Change Language" type="button">
|
||||||
|
<div class={styles.flagIcon}>
|
||||||
|
<Dynamic component={LOCALE_FLAGS[locale()]} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Added a wrapper div for list styling */}
|
||||||
|
<div class={styles.menu}>
|
||||||
|
<For each={Object.keys(LOCALE_FLAGS)}>
|
||||||
|
{(lang) => (
|
||||||
|
<button class={`${styles.option} ${locale() === lang ? styles.active : ""}`} onClick={() => setLocale(lang as Locale)} type="button">
|
||||||
|
<span class={styles.flag}>
|
||||||
|
<Dynamic component={LOCALE_FLAGS[lang]} />
|
||||||
|
</span>
|
||||||
|
<span class={styles.label}>{lang.toUpperCase()}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default I18nSelector;
|
||||||
24
Frontend/src/components/SVGs/Flags/FlagHK.tsx
Normal file
24
Frontend/src/components/SVGs/Flags/FlagHK.tsx
Normal file
@ -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<ComponentProps<"svg"> & { size?: number }> = (props) => {
|
||||||
|
const [local, others] = splitProps(mergeProps({ size: 24 }, props), ["size"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={local.size} height={local.size} xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480" {...others}>
|
||||||
|
<path fill="#EC1B2E" d="M0 0h640v480H0" />
|
||||||
|
<path
|
||||||
|
id="hk_svg__a"
|
||||||
|
fill="#fff"
|
||||||
|
d="M346.3 103.1C267 98 230.6 201.9 305.6 240.3c-26-22.4-20.6-55.3-10.1-72.4l1.9 1.1c-13.8 23.5-11.2 52.7 11.1 71-12.7-12.3-9.5-39 12.1-48.9s23.6-39.3 16.4-49.1q-14.7-25.6 9.3-38.9M307.9 164l-4.7 7.4-1.8-8.6-8.6-2.3 7.8-4.3-.6-8.9 6.5 6.1 8.3-3.3-3.7 8.1 5.6 6.8z"
|
||||||
|
/>
|
||||||
|
<use href="#hk_svg__a" transform="rotate(72 312.5 243.5)" />
|
||||||
|
<use href="#hk_svg__a" transform="rotate(144 312.5 243.5)" />
|
||||||
|
<use href="#hk_svg__a" transform="rotate(216 312.5 243.5)" />
|
||||||
|
<use href="#hk_svg__a" transform="rotate(288 312.5 243.5)" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagHK;
|
||||||
25
Frontend/src/components/SVGs/Flags/FlagUS.tsx
Normal file
25
Frontend/src/components/SVGs/Flags/FlagUS.tsx
Normal file
@ -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<ComponentProps<"svg"> & { size?: number }> = (props) => {
|
||||||
|
const [local, others] = splitProps(mergeProps({ size: 24 }, props), ["size"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={local.size} height={local.size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" {...others}>
|
||||||
|
<path fill="#bd3d44" d="M0 0h640v480H0" />
|
||||||
|
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640" />
|
||||||
|
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
|
||||||
|
<marker id="us_svg__a" markerHeight="30" markerWidth="30">
|
||||||
|
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
|
||||||
|
</marker>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
marker-mid="url(#us_svg__a)"
|
||||||
|
d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagUS;
|
||||||
16
Frontend/src/components/SVGs/Moon.tsx
Normal file
16
Frontend/src/components/SVGs/Moon.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Path: Frontend/src/components/SVGs/Moon.tsx
|
||||||
|
|
||||||
|
const Moon = () => {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M14.228 7.9439C10.5176 8.82869 7.75757 12.1054 7.75757 15.9987C7.75757 20.5716 11.5618 24.2919 16.2367 24.2919C19.2323 24.2919 21.9337 22.7699 23.4514 20.3585C23.2779 20.3676 23.1033 20.3722 22.9287 20.3722C17.7826 20.3722 13.5951 16.2772 13.5951 11.2435C13.5951 10.1032 13.8108 8.98914 14.228 7.9439M16.2367 26.4993C10.3171 26.4993 5.50037 21.7899 5.50037 15.9987C5.50037 10.2109 10.3171 5.49927 16.2367 5.49927C16.6598 5.49927 17.0501 5.72963 17.2435 6.09753C17.438 6.46428 17.4087 6.90668 17.1638 7.24363C16.3059 8.42297 15.8535 9.80631 15.8535 11.2435C15.8535 15.06 19.0272 18.1637 22.9287 18.1637C23.6483 18.1637 24.3573 18.0582 25.0359 17.8531C25.4378 17.7293 25.8785 17.8359 26.1738 18.1304C26.4715 18.425 26.5758 18.8559 26.4446 19.2467C25.0019 23.5847 20.9 26.4993 16.2367 26.4993"
|
||||||
|
fill="white"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Moon;
|
||||||
29
Frontend/src/components/SVGs/MouseClick.module.scss
Normal file
29
Frontend/src/components/SVGs/MouseClick.module.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Frontend/src/components/SVGs/MouseClick.tsx
Normal file
31
Frontend/src/components/SVGs/MouseClick.tsx
Normal file
@ -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> = (props) => {
|
||||||
|
const merged = mergeProps({ animate: true }, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
class={`${styles["mouse-click-icon"]} ${merged.animate ? styles.animate : ""} ${merged.class ?? ""}`}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width={0}
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12.6 5c-0.2 0-0.5 0-0.6 0 0-0.2-0.2-0.6-0.4-0.8s-0.6-0.4-1.1-0.4c-0.2 0-0.4 0-0.6 0.1-0.1-0.2-0.2-0.3-0.3-0.5-0.2-0.2-0.5-0.4-1.1-0.4-0.2 0-0.4 0-0.5 0.1v-1.7c0-0.6-0.4-1.4-1.4-1.4-0.4 0-0.8 0.2-1.1 0.4-0.5 0.6-0.5 1.4-0.5 1.4v4.3c-0.6 0.1-1.1 0.3-1.4 0.6-0.6 0.7-0.6 1.6-0.6 2.8 0 0.2 0 0.5 0 0.7 0 1.4 0.7 2.1 1.4 2.8l0.3 0.4c1.3 1.2 2.4 1.6 5.1 1.6 2.9 0 4.2-1.6 4.2-5.1v-2.5c0-0.7-0.2-2.1-1.4-2.4zM13 7.4v2.6c0 3.4-1.3 4.1-3.2 4.1-2.4 0-3.3-0.3-4.3-1.3-0.1-0.1-0.2-0.2-0.4-0.4-0.7-0.8-1.1-1.2-1.1-2.2 0-0.2 0-0.5 0-0.7 0-1 0-1.7 0.3-2.1 0.1-0.1 0.4-0.2 0.7-0.2v0.5l-0.3 1.5c0 0.1 0 0.1 0.1 0.2s0.2 0 0.2 0l1-1.2c0-0.1 0-0.2 0-0.2v-6.2c0-0.1 0-0.5 0.2-0.7 0.1 0 0.2-0.1 0.4-0.1 0.3 0 0.4 0.3 0.4 0.4v3.1c0 0 0 0 0 0v1.2c0 0.3 0.2 0.6 0.5 0.6s0.5-0.3 0.5-0.5v-1.3c0 0 0 0 0 0 0-0.1 0.1-0.5 0.5-0.5 0.3 0 0.5 0.1 0.5 0.4v1.3c0 0.3 0.2 0.6 0.5 0.6s0.5-0.3 0.5-0.5v-0.7c0-0.1 0.1-0.3 0.5-0.3 0.2 0 0.3 0.1 0.3 0.1 0.2 0.1 0.2 0.4 0.2 0.4v0.8c0 0.3 0.2 0.5 0.4 0.5 0.3 0 0.5-0.1 0.5-0.4 0-0.1 0.1-0.2 0.2-0.3 0 0 0.1 0 0.2 0 0.6 0.2 0.7 1.2 0.7 1.5 0-0.1 0-0.1 0 0z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MouseClick;
|
||||||
64
Frontend/src/components/SVGs/Sun.tsx
Normal file
64
Frontend/src/components/SVGs/Sun.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Path: Frontend/src/components/SVGs/Sun.tsx
|
||||||
|
|
||||||
|
const Sun = () => {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M16.0003 21.4194C13.0123 21.4194 10.5813 18.9874 10.5813 15.9994C10.5813 13.0114 13.0123 10.5804 16.0003 10.5804C18.9883 10.5804 21.4193 13.0114 21.4193 15.9994C21.4193 18.9874 18.9883 21.4194 16.0003 21.4194M16.0003 8.64136C11.9423 8.64136 8.64233 11.9414 8.64233 15.9994C8.64233 20.0574 11.9423 23.3574 16.0003 23.3574C20.0573 23.3574 23.3583 20.0574 23.3583 15.9994C23.3583 11.9414 20.0573 8.64136 16.0003 8.64136"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M6.11559 15.0298H3.34559C2.81059 15.0298 2.37659 15.4648 2.37659 15.9998C2.37659 16.5348 2.81059 16.9688 3.34559 16.9688H6.11559C6.65159 16.9688 7.08459 16.5348 7.08459 15.9998C7.08459 15.4648 6.65159 15.0298 6.11559 15.0298"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M16.0004 7.08447C16.5364 7.08447 16.9704 6.64946 16.9704 6.11446V3.34546C16.9704 2.81046 16.5364 2.37646 16.0004 2.37646C15.4644 2.37646 15.0304 2.81046 15.0304 3.34546V6.11446C15.0304 6.64946 15.4644 7.08447 16.0004 7.08447"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M16.0004 24.9146C15.4644 24.9146 15.0304 25.3496 15.0304 25.8846V28.6536C15.0304 29.1886 15.4644 29.6236 16.0004 29.6236C16.5364 29.6236 16.9704 29.1886 16.9704 28.6536V25.8846C16.9704 25.3496 16.5364 24.9146 16.0004 24.9146"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M28.6542 15.0298H25.8842C25.3492 15.0298 24.9152 15.4648 24.9152 15.9998C24.9152 16.5348 25.3492 16.9688 25.8842 16.9688H28.6542C29.1902 16.9688 29.6242 16.5348 29.6242 15.9998C29.6242 15.4648 29.1902 15.0298 28.6542 15.0298"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M22.9896 9.97995C23.2376 9.97995 23.4856 9.88495 23.6756 9.69595L24.7036 8.66795C25.0816 8.28995 25.0816 7.67495 24.7036 7.29595C24.3246 6.91795 23.7106 6.91795 23.3316 7.29595L22.3036 8.32495C21.9256 8.70295 21.9256 9.31695 22.3036 9.69595C22.4926 9.88495 22.7416 9.97995 22.9896 9.97995"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M8.32507 9.69593C8.51407 9.88493 8.76207 9.97993 9.01107 9.97993C9.25907 9.97993 9.50707 9.88493 9.69607 9.69593C10.0751 9.31693 10.0751 8.70293 9.69607 8.32493L8.66807 7.29693C8.28907 6.91893 7.67507 6.91893 7.29707 7.29693C6.91807 7.67493 6.91807 8.28993 7.29707 8.66793L8.32507 9.69593Z"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M8.32507 22.3043L7.29707 23.3313C6.91807 23.7093 6.91807 24.3243 7.29707 24.7023C7.48607 24.8923 7.73407 24.9873 7.98207 24.9873C8.23007 24.9873 8.47807 24.8923 8.66807 24.7023L9.69607 23.6753C10.0751 23.2973 10.0751 22.6833 9.69607 22.3043C9.31807 21.9253 8.70307 21.9253 8.32507 22.3043"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M23.6752 22.3043C23.2962 21.9253 22.6822 21.9253 22.3032 22.3043C21.9252 22.6833 21.9252 23.2973 22.3042 23.6753L23.3322 24.7023C23.5212 24.8923 23.7692 24.9873 24.0182 24.9873C24.2662 24.9873 24.5142 24.8923 24.7032 24.7023C25.0822 24.3243 25.0822 23.7093 24.7032 23.3313L23.6752 22.3043Z"
|
||||||
|
fill="black"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sun;
|
||||||
4
Frontend/src/entry-client.tsx
Normal file
4
Frontend/src/entry-client.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
21
Frontend/src/entry-server.tsx
Normal file
21
Frontend/src/entry-server.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||||
|
|
||||||
|
export default createHandler(() => (
|
||||||
|
<StartServer
|
||||||
|
document={({ assets, children, scripts }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
{assets}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">{children}</div>
|
||||||
|
{scripts}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
3
Frontend/src/global.d.ts
vendored
Normal file
3
Frontend/src/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Path: Frontend/src/global.d.ts
|
||||||
|
|
||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
18
Frontend/src/helpers/env.ts
Normal file
18
Frontend/src/helpers/env.ts
Normal file
@ -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;
|
||||||
11
Frontend/src/i18n/config.ts
Normal file
11
Frontend/src/i18n/config.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
51
Frontend/src/i18n/context.tsx
Normal file
51
Frontend/src/i18n/context.tsx
Normal file
@ -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<typeof dictionaries.en>;
|
||||||
|
isReady: () => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const I18nContext = createContext<I18nContextType>();
|
||||||
|
|
||||||
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
|
const [locale, setLocaleSignal] = createSignal<Locale>("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 <I18nContext.Provider value={{ locale, setLocale, t, isReady }}>{props.children}</I18nContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
return useContext(I18nContext)!;
|
||||||
|
}
|
||||||
30
Frontend/src/i18n/en.ts
Normal file
30
Frontend/src/i18n/en.ts
Normal file
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
30
Frontend/src/i18n/zh.ts
Normal file
30
Frontend/src/i18n/zh.ts
Normal file
@ -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: "登入",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
15
Frontend/src/routes/404.module.scss
Normal file
15
Frontend/src/routes/404.module.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Frontend/src/routes/[...404].tsx
Normal file
26
Frontend/src/routes/[...404].tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<main class={styles.notFound}>
|
||||||
|
<h3>{t("404").message}</h3>
|
||||||
|
<A href="/">
|
||||||
|
<button class={styles.backHomeBtn}>{t("404").backHome}</button>
|
||||||
|
</A>
|
||||||
|
</main>
|
||||||
|
<TopRightSimple />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
0
Frontend/src/routes/auth/forgot-password/index.tsx
Normal file
0
Frontend/src/routes/auth/forgot-password/index.tsx
Normal file
45
Frontend/src/routes/auth/login/index.tsx
Normal file
45
Frontend/src/routes/auth/login/index.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<main class={styles.loginMain}>
|
||||||
|
<h1>{t("auth").login.title}</h1>
|
||||||
|
<form>
|
||||||
|
<label for="username">{t("auth").login.usernameLabel}</label>
|
||||||
|
<input type="text" id="username" name="username" required />
|
||||||
|
<label for="password">{t("auth").login.passwordLabel}</label>
|
||||||
|
<input type="password" id="password" name="password" required />
|
||||||
|
<button type="submit" class={styles.loginBtn}>
|
||||||
|
{t("auth").login.loginBtn}
|
||||||
|
</button>
|
||||||
|
<span class={styles.links}>
|
||||||
|
<A href="auth/forgot-password">{t("auth").login.forgotPassword}</A>
|
||||||
|
</span>
|
||||||
|
<span class={styles.links}>
|
||||||
|
{t("auth").login.noAccount} <A href="auth/register">{t("auth").login.registerLink}</A>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Federated */}
|
||||||
|
<div class={styles.federatedLogin}>
|
||||||
|
<GoogleBtn class={styles.federatedBtn} />
|
||||||
|
<GoogleBtn class={styles.federatedBtn} />
|
||||||
|
<GoogleBtn class={styles.federatedBtn} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<TopRightSimple />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
132
Frontend/src/routes/auth/login/login.module.scss
Normal file
132
Frontend/src/routes/auth/login/login.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
Frontend/src/routes/auth/signup/index.tsx
Normal file
0
Frontend/src/routes/auth/signup/index.tsx
Normal file
0
Frontend/src/routes/dashboard.tsx
Normal file
0
Frontend/src/routes/dashboard.tsx
Normal file
0
Frontend/src/routes/dashboard/index.tsx
Normal file
0
Frontend/src/routes/dashboard/index.tsx
Normal file
0
Frontend/src/routes/dashboard/org/create/index.tsx
Normal file
0
Frontend/src/routes/dashboard/org/create/index.tsx
Normal file
0
Frontend/src/routes/dashboard/profile/index.tsx
Normal file
0
Frontend/src/routes/dashboard/profile/index.tsx
Normal file
21
Frontend/src/routes/index.module.scss
Normal file
21
Frontend/src/routes/index.module.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Frontend/src/routes/index.tsx
Normal file
29
Frontend/src/routes/index.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<main class={styles.homepage}>
|
||||||
|
<h1>Moku (木)</h1>
|
||||||
|
<p>{t("home").description}</p>
|
||||||
|
<A href="/auth/login">
|
||||||
|
<button class={styles.loginBtn}> {t("home").loginBtn} </button>
|
||||||
|
</A>
|
||||||
|
<MouseClick animate={true} />
|
||||||
|
</main>
|
||||||
|
<TopRightSimple />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LandingPage;
|
||||||
15
Frontend/src/styles/_fonts.scss
Normal file
15
Frontend/src/styles/_fonts.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
111
Frontend/src/styles/_mixins.scss
Normal file
111
Frontend/src/styles/_mixins.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Frontend/src/styles/_reset.scss
Normal file
52
Frontend/src/styles/_reset.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
93
Frontend/src/styles/main.scss
Normal file
93
Frontend/src/styles/main.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
85
Frontend/src/styles/theme.scss
Normal file
85
Frontend/src/styles/theme.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
14
Frontend/src/styles/transitions.scss
Normal file
14
Frontend/src/styles/transitions.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
19
Frontend/tsconfig.json
Normal file
19
Frontend/tsconfig.json
Normal file
@ -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/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user