Compare commits

...

10 Commits

Author SHA1 Message Date
587a463623 feat: dark theme support 2025-05-24 16:15:22 +02:00
1a596eef87 feat: no rounded corners 2025-05-24 16:15:12 +02:00
e6b87a6561 feat: button variants 2025-05-24 16:15:00 +02:00
3bcc5f8900 fix: database wasn't opening 2025-05-24 16:14:53 +02:00
c5ee912408 feat: dark overlay in assets 2025-05-24 16:14:44 +02:00
9766da7cfd feat: icon in assets 2025-05-24 16:14:37 +02:00
a004a82272 feat: dark and light overlays in public 2025-05-24 16:14:25 +02:00
64ca9b922e feat: allow all hosts on dev 2025-05-24 16:14:15 +02:00
38a2ce1ce9 feat: start agreement page 2025-05-24 14:31:26 +02:00
accde2662f feat: account list 2025-05-24 14:31:16 +02:00
14 changed files with 252 additions and 63 deletions

BIN
web/public/dark-overlay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

BIN
web/public/overlay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View File

@ -6,12 +6,17 @@ import LoginPage from "./pages/Login";
import RegisterPage from "./pages/Register";
import { useDbContext } from "./context/db/db";
import { openDB } from "idb";
import AgreementPage from "./pages/Agreement";
const router = createBrowserRouter([
{
path: "/",
element: <IndexPage />,
},
{
path: "/agreement",
element: <AgreementPage />,
},
{
path: "/login",
element: <LoginPage />,
@ -27,11 +32,15 @@ const App: FC = () => {
useEffect(() => {
const openConnection = async () => {
const conn = await openDB("guard-local", 3);
const dbPromise = openDB("guard-local", 3, {
upgrade: (db) => {
if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "accountId" });
}
},
});
if (!conn.objectStoreNames.contains("accounts")) {
conn.createObjectStore("accounts", { keyPath: "accountId" });
}
const conn = await dbPromise;
setDb(conn);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

BIN
web/src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,20 +1,40 @@
import type { ButtonHTMLAttributes, FC, ReactNode } from "react";
import {
useMemo,
type ButtonHTMLAttributes,
type FC,
type ReactNode,
} from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
className?: string;
loading?: boolean;
variant?: "contained" | "outlined" | "text";
}
export const Button: FC<ButtonProps> = ({
children,
className,
loading,
variant = "contained",
...props
}) => {
const appearance = useMemo(() => {
switch (variant) {
case "contained":
return "bg-blue-600 text-white hover:bg-blue-700";
case "outlined":
return "border border-blue-600 text-blue-600 hover:text-blue-700 font-medium";
case "text":
return "text-blue-600 hover:text-blue-700 font-medium";
}
return "";
}, [variant]);
return (
<button
className={`bg-blue-600 text-white cursor-pointer py-2 px-4 rounded-md hover:bg-blue-700 transition-colors ${
className={`cursor-pointer py-2 px-4 rounded-md transition-colors ${appearance} ${
className || ""
}${
loading

View File

@ -7,7 +7,7 @@ interface ComponentProps {
export const Card: FC<ComponentProps> = ({ children, className }) => {
return (
<div className={`bg-white rounded-lg shadow-md ${className || ""}`}>
<div className={`bg-white sm:rounded-lg shadow-md ${className || ""}`}>
{children}
</div>
);

View File

@ -6,7 +6,7 @@ export const Input: FC<InputProps> = ({ className, ...props }) => {
return (
<input
{...props}
className={`w-full border border-gray-300 rounded-md px-3 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
className={`w-full border border-gray-300 dark:border-gray-600 dark:placeholder-gray-600 dark:text-gray-100 rounded-md px-3 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
className || ""
}`}
/>

View File

@ -0,0 +1,79 @@
import { useDbContext } from "@/context/db/db";
import { type LocalAccount, useAccountRepo } from "@/repository/account";
import { CirclePlus, User } from "lucide-react";
import { useEffect, useState, type FC } from "react";
const AccountList: FC = () => {
const [accounts, setAccounts] = useState<LocalAccount[]>([]);
const repo = useAccountRepo();
const { connected } = useDbContext();
useEffect(() => {
if (connected) repo.loadAll().then(setAccounts);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connected]);
if (!connected) {
return (
<div className="p-5 flex-1 h-full flex-full flex items-center justify-center">
<div role="status">
<svg
aria-hidden="true"
className="w-12 h-12 text-gray-200 dark:text-gray-600 animate-spin fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
</div>
);
}
return (
<>
{accounts.map((account) => (
<div
key={account.accountId}
className="flex flex-row items-center p-4 border-gray-200 dark:border-gray-700/65 border-b border-r-0 border-l-0 select-none cursor-pointer hover:bg-gray-50/50 dark:hover:bg-gray-800/10 transition-colors mb-0"
>
<div>
<div className="rounded-full p-2 bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-200 mr-3 ring ring-gray-900 dark:ring dark:ring-gray-100">
<User />
</div>
</div>
<div className="flex flex-col">
<p className="text-base text-gray-900 dark:text-gray-200">
{account.label}
</p>
<p className="text-gray-500 dark:text-gray-600 text-sm">
{account.email}
</p>
</div>
</div>
))}
<div className="flex flex-row items-center p-4 border-gray-200 dark:border-gray-700/65 border-b border-r-0 border-l-0 select-none cursor-pointer hover:bg-gray-50/50 dark:hover:bg-gray-800/10 transition-colors mb-0">
<div>
<div className="rounded-full p-2 text-gray-900 dark:text-gray-200 mr-3">
<CirclePlus />
</div>
</div>
<p className="text-base text-gray-900 dark:text-gray-200">
Add new account
</p>
</div>
</>
);
};
export default AccountList;

View File

@ -0,0 +1,108 @@
import { type FC } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { ArrowLeftRight, User } from "lucide-react";
import { Button } from "@/components/ui/button";
const AgreementPage: FC = () => {
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-[425px] sm:min-w-[425px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">
<div className="flex flex-col items-center pt-10 sm:pt-0">
<div className="flex flex-col items-center flex-5/6">
{/* <img
src="/icon.png"
alt="icon"
className="w-16 h-16 mb-4 mt-2 sm:mt-6"
/> */}
<div className="flex flex-row items-center gap-4 mt-2 mb-4 sm:mt-6">
<div className="p-2 bg-gray-100 rounded-full ring ring-gray-900 dark:ring dark:ring-gray-100">
<User size={32} />
</div>
<div className="text-gray-400 dark:text-gray-600">
{/* <Activity /> */}
<ArrowLeftRight />
</div>
<div className="p-2 rounded-full bg-gray-900 ring ring-gray-900 dark:ring dark:ring-gray-100">
{/* <img
src="https://lucide.dev/logo.dark.svg"
className="w-8 h-8"
/> */}
<img
src="https://developer.mozilla.org/favicon.svg"
className="w-8 h-8"
/>
</div>
</div>
<div className="px-4 sm:mt-4 mt-8">
<h2 className="text-2xl font-medium text-gray-800 dark:text-gray-300 text-center w-full mb-2">
<a href="#" className="text-blue-500">
MDN Lab Services
</a>{" "}
wants to access your Home Account
</h2>
<div className="flex flex-row items-center justify-center mb-6 gap-2">
<div className="p-2 bg-gray-100 rounded-full ring ring-gray-900 dark:ring dark:ring-gray-100">
<User />
</div>
<p className="text-sm text-gray-500 dark:text-gray-500">
qwer.009771@gmail.com
</p>
</div>
<h4 className="text-base mb-3 text-gray-400 dark:text-gray-500 text-left">
This will allow{" "}
<a href="#" className="text-blue-500">
MDN Lab Services
</a>{" "}
to:
</h4>
</div>
</div>
{/* <LogIn className="w-8 h-8 text-gray-700 mb-4" /> */}
<CardContent className="w-full space-y-4 text-sm">
<div className="flex flex-col gap-3 mb-8">
<div className="flex flex-row items-center justify-between text-gray-600 dark:text-gray-400">
<div className="flex flex-row items-center gap-4">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<p>View your full name, email and profile image</p>
</div>
</div>
<div className="flex flex-row items-center justify-between text-gray-600 dark:text-gray-400">
<div className="flex flex-row items-center gap-4">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<p>View your permission from "MDN" group</p>
</div>
</div>
</div>
<div className="mb-10">
<p className="font-medium mb-4 dark:text-gray-200">
Are you sure you want to trust MDN Lab Services?
</p>
<p className="text-sm text-gray-400 dark:text-gray-500">
Please do not share any sensitive, personal, or unnecessary
information unless you trust this service. Protect your
privacy and only provide information that is required for the
intended purpose.
</p>
</div>
<div className="flex flex-row justify-between items-center">
<Button variant="text">Cancel</Button>
<Button>Allow</Button>
</div>
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export default AgreementPage;

View File

@ -1,31 +1,22 @@
import { useDbContext } from "@/context/db/db";
import { useAccountRepo, type LocalAccount } from "@/repository/account";
import { useEffect, useState, type FC } from "react";
import { type FC } from "react";
// import overlay from "@/assets/overlay.jpg";
// import darkOverlay from "@/assets/dark-overlay.jpg";
import overlay from "@/assets/overlay.jpg";
import { Card, CardContent } from "@/components/ui/card";
import { User } from "lucide-react";
import AccountList from "@/feature/AccountList";
const IndexPage: FC = () => {
const [accounts, setAccounts] = useState<LocalAccount[]>([]);
const repo = useAccountRepo();
const { connected } = useDbContext();
useEffect(() => {
if (connected) repo.loadAll().then(setAccounts);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connected]);
// console.log(overlay);
return (
<div
className="relative min-h-screen bg-cover bg-center bg-white"
style={{ backgroundImage: `url(${overlay})` }}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
// style={{ backgroundImage: `url(${overlay})` }}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-[700px] sm:min-w-[700px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/90 backdrop-blur-md">
<div className="flex sm:flex-row flex-col sm:items-start items-center pt-16 sm:pt-0">
<div className="flex flex-col items-center flex-5/6">
<Card className="sm:w-[700px] sm:min-w-[700px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">
<div className="flex sm:flex-row flex-col sm:items-stretch items-center pt-16 sm:pt-0">
<div className="flex flex-col items-center flex-1">
<img
src="/icon.png"
alt="icon"
@ -33,10 +24,10 @@ const IndexPage: FC = () => {
/>
<div className="px-4 sm:mt-4 mt-8">
<h2 className="text-2xl font-bold text-gray-800 text-left w-full">
<h2 className="text-2xl font-bold text-gray-800 text-left w-full dark:text-gray-100">
Select Account
</h2>
<h4 className="text-base mb-3 text-gray-400 text-left">
<h4 className="text-base mb-3 text-gray-400 text-left dark:text-gray-300">
Choose one of the accounts below in order to proceed to home
lab services and tools.
</h4>
@ -44,23 +35,8 @@ const IndexPage: FC = () => {
</div>
{/* <LogIn className="w-8 h-8 text-gray-700 mb-4" /> */}
<CardContent className="w-full space-y-4">
{accounts.map((account) => (
<div
key={account.accountId}
className="flex flex-row items-center p-4 border-gray-200 border sm:border-l sm:border-r border-r-0 border-l-0 sm:rounded-xl select-none cursor-pointer hover:bg-gray-50 transition-colors mb-2"
>
<div>
<div className="rounded-full p-2 bg-gray-100 mr-3">
<User />
</div>
</div>
<div className="flex flex-col">
<p className="text-base">{account.label}</p>
<p className="text-gray-500 text-sm">{account.email}</p>
</div>
</div>
))}
<CardContent className="w-full space-y-4 flex-1">
<AccountList />
</CardContent>
</div>
</Card>

View File

@ -5,7 +5,6 @@ import { Link } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import overlay from "@/assets/overlay.jpg";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useCallback, useState } from "react";
import { loginApi } from "@/api/login";
@ -71,11 +70,10 @@ export default function LoginPage() {
return (
<div
className="relative min-h-screen bg-cover bg-center bg-white"
style={{ backgroundImage: `url(${overlay})` }}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/90 backdrop-blur-md">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/70 backdrop-blur-md">
<div className="flex flex-col items-center pt-16 sm:pt-0">
<img
src="/icon.png"
@ -84,10 +82,10 @@ export default function LoginPage() {
/>
<div className="px-4 sm:mt-4 mt-8">
<h2 className="text-2xl font-bold text-gray-800 text-left w-full">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-200 text-left w-full">
Sign In
</h2>
<h4 className="text-base mb-3 text-gray-400 text-left">
<h4 className="text-base mb-3 text-gray-400 dark:text-gray-500 text-left">
Enter your credentials to access home services and tools.
</h4>
</div>
@ -96,7 +94,7 @@ export default function LoginPage() {
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-600 w-4 h-4" />
<Input
id="email"
type="email"
@ -121,7 +119,7 @@ export default function LoginPage() {
<div className="mb-4">
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-600 w-4 h-4" />
<Input
id="password"
type="password"
@ -147,7 +145,7 @@ export default function LoginPage() {
)}
{error.length > 0 && (
<div className="border border-red-400 p-2 rounded bg-red-200 text-sm">
<div className="border border-red-400 p-2 rounded bg-red-200 dark:border-red-600 dark:bg-red-400 text-sm">
{error}
</div>
)}

View File

@ -4,7 +4,6 @@ import { Link } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import overlay from "@/assets/overlay.jpg";
import { useCallback, useState } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
@ -75,11 +74,10 @@ export default function RegisterPage() {
return (
<div
className="relative min-h-screen bg-cover bg-center bg-white"
style={{ backgroundImage: `url(${overlay})` }}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/90 backdrop-blur-md">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">
<div className="flex flex-col items-center pt-16 sm:pt-0">
<img
src="/icon.png"
@ -88,10 +86,10 @@ export default function RegisterPage() {
/>
<div className="px-4 sm:mt-4 mt-8">
<h2 className="text-2xl font-bold text-gray-800 text-left w-full">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-200 text-left w-full">
Sign Up
</h2>
<h4 className="text-base mb-3 text-gray-400 text-left">
<h4 className="text-base mb-3 text-gray-400 dark:text-gray-600 text-left">
Fill up this form to start using homelab services and tools.
</h4>
</div>
@ -235,7 +233,7 @@ export default function RegisterPage() {
)}
{error.length > 0 && (
<div className="border border-red-400 p-2 rounded bg-red-200 text-sm">
<div className="border border-red-400 p-2 rounded bg-red-200 dark:border-red-600 dark:bg-red-400 text-sm">
{error}
</div>
)}

View File

@ -16,6 +16,7 @@ export default defineConfig(({ mode }) => ({
secure: false,
},
},
allowedHosts: true,
}
: undefined,
build: {