UI overhaul

This commit is contained in:
voldemort 2025-07-24 11:43:01 +07:00
parent cc3b37db1d
commit f83d22c09e
18 changed files with 1164 additions and 744 deletions

View File

@ -4,5 +4,6 @@
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"useTabs": false, "useTabs": false,
"printWidth": 100 "printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
} }

View File

@ -12,8 +12,10 @@
"axios": "^1.10.0", "axios": "^1.10.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0", "react-router-dom": "^7.7.0",
"shaka-player": "^4.15.8", "shaka-player": "^4.15.8",
"sonner": "^2.0.6",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
@ -22,10 +24,12 @@
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0", "@vitejs/plugin-react-swc": "^3.11.0",
"daisyui": "^5.0.46",
"eslint": "^9.31.0", "eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"vite": "^7.0.5" "vite": "^7.0.5"
} }
}, },
@ -2137,6 +2141,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/daisyui": {
"version": "5.0.46",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.46.tgz",
"integrity": "sha512-vMDZK1tI/bOb2Mc3Mk5WpquBG3ZqBz1YKZ0xDlvpOvey60dOS4/5Qhdowq1HndbQl7PgDLDYysxAjjUjwR7/eQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -3479,6 +3493,110 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.14",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
"integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.21.3"
},
"peerDependencies": {
"@ianvs/prettier-plugin-sort-imports": "*",
"@prettier/plugin-hermes": "*",
"@prettier/plugin-oxc": "*",
"@prettier/plugin-pug": "*",
"@shopify/prettier-plugin-liquid": "*",
"@trivago/prettier-plugin-sort-imports": "*",
"@zackad/prettier-plugin-twig": "*",
"prettier": "^3.0",
"prettier-plugin-astro": "*",
"prettier-plugin-css-order": "*",
"prettier-plugin-import-sort": "*",
"prettier-plugin-jsdoc": "*",
"prettier-plugin-marko": "*",
"prettier-plugin-multiline-arrays": "*",
"prettier-plugin-organize-attributes": "*",
"prettier-plugin-organize-imports": "*",
"prettier-plugin-sort-imports": "*",
"prettier-plugin-style-order": "*",
"prettier-plugin-svelte": "*"
},
"peerDependenciesMeta": {
"@ianvs/prettier-plugin-sort-imports": {
"optional": true
},
"@prettier/plugin-hermes": {
"optional": true
},
"@prettier/plugin-oxc": {
"optional": true
},
"@prettier/plugin-pug": {
"optional": true
},
"@shopify/prettier-plugin-liquid": {
"optional": true
},
"@trivago/prettier-plugin-sort-imports": {
"optional": true
},
"@zackad/prettier-plugin-twig": {
"optional": true
},
"prettier-plugin-astro": {
"optional": true
},
"prettier-plugin-css-order": {
"optional": true
},
"prettier-plugin-import-sort": {
"optional": true
},
"prettier-plugin-jsdoc": {
"optional": true
},
"prettier-plugin-marko": {
"optional": true
},
"prettier-plugin-multiline-arrays": {
"optional": true
},
"prettier-plugin-organize-attributes": {
"optional": true
},
"prettier-plugin-organize-imports": {
"optional": true
},
"prettier-plugin-sort-imports": {
"optional": true
},
"prettier-plugin-style-order": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
}
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -3516,6 +3634,15 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@ -3667,6 +3794,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sonner": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@ -14,8 +14,10 @@
"axios": "^1.10.0", "axios": "^1.10.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0", "react-router-dom": "^7.7.0",
"shaka-player": "^4.15.8", "shaka-player": "^4.15.8",
"sonner": "^2.0.6",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
@ -24,10 +26,12 @@
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0", "@vitejs/plugin-react-swc": "^3.11.0",
"daisyui": "^5.0.46",
"eslint": "^9.31.0", "eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"vite": "^7.0.5" "vite": "^7.0.5"
} }
} }

View File

@ -1,48 +1,19 @@
import { useState } from "react"; import { Route, Routes } from "react-router-dom";
import Home from "./components/Pages/HomePage";
import Cache from "./components/Pages/Cache";
import API from "./components/Pages/API";
import TestPlayer from "./components/Pages/TestPlayer";
import NavBar from "./components/NavBar";
import NavBarMain from "./components/NavBarMain";
import SideMenu from "./components/SideMenu"; // Add this import
import Account from "./components/Pages/Account"; import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom"; import API from "./components/Pages/API";
import Cache from "./components/Pages/Cache";
import Home from "./components/Pages/HomePage";
import TestPlayer from "./components/Pages/TestPlayer";
function App() { function App() {
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
return ( return (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black"> <Routes>
{/* The SideMenu should be visible when isMenuOpen is true */} <Route path="/" element={<Home />} />
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} /> <Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<div <Route path="/testplayer" element={<TestPlayer />} />
id="navbarcontainer" <Route path="/account" element={<Account />} />
className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0" </Routes>
>
<NavBar />
</div>
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
<div
id="navbarmaincontainer"
className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"
>
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
</div>
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes>
</div>
</div>
</div>
); );
} }

Binary file not shown.

View File

@ -0,0 +1,15 @@
@font-face {
font-family: Inter;
src: url("./InterVariable.woff2");
font-style: normal;
font-weight: 300 900;
font-display: swap;
}
@font-face {
font-family: Inter;
src: url("./InterVariable-Italic.woff2");
font-style: italic;
font-weight: 300 900;
font-display: swap;
}

View File

@ -0,0 +1,9 @@
const Container = ({ children, className = "", ...props }) => {
return (
<main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
{children}
</main>
);
};
export default Container;

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import homeIcon from "../assets/icons/home.svg"; import { FaDiscord } from "react-icons/fa";
import cacheIcon from "../assets/icons/cache.svg"; import { FaTelegram } from "react-icons/fa";
import apiIcon from "../assets/icons/api.svg"; import { SiGitea } from "react-icons/si";
import testPlayerIcon from "../assets/icons/testplayer.svg"; import { FaHome } from "react-icons/fa";
import accountIcon from "../assets/icons/account.svg"; import { FaDatabase } from "react-icons/fa";
import discordIcon from "../assets/icons/discord.svg"; import { IoCodeSlashSharp } from "react-icons/io5";
import telegramIcon from "../assets/icons/telegram.svg"; import { FaVideo } from "react-icons/fa";
import giteaIcon from "../assets/icons/gitea.svg"; import { RiAccountCircleFill } from "react-icons/ri";
function NavBar() { function NavBar() {
const [externalLinks, setExternalLinks] = useState({ const [externalLinks, setExternalLinks] = useState({
@ -23,152 +23,156 @@ function NavBar() {
.catch((error) => console.error("Error fetching links:", error)); .catch((error) => console.error("Error fetching links:", error));
}, []); }, []);
const MenuItem = ({ to, children }) => {
return (
<li>
<NavLink to={to} className={({ isActive }) => (isActive ? "menu-active" : "")}>
{children}
</NavLink>
</li>
);
};
return ( return (
<div className="flex flex-col w-full h-full bg-white/1"> <>
{/* Header */} <div className="navbar sticky top-0 z-300 bg-slate-700 shadow-sm text-white">
<div> <div className="navbar-start">
<p className="text-white text-2xl font-bold p-3 text-center mb-5"> <div className="dropdown">
<a href="/">CDRM-Project</a> <div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
</p> <svg
</div> xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
{" "}
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h8m-8 6h16"
/>{" "}
</svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
>
<MenuItem to="/">
<FaHome alt="Home" width={20} height={20} />
Home
</MenuItem>
<MenuItem to="/cache">
<FaDatabase alt="Cache" width={20} height={20} />
Cache
</MenuItem>
<MenuItem to="/api">
<IoCodeSlashSharp alt="API" width={20} height={20} />
API
</MenuItem>
<MenuItem to="/testplayer">
<FaVideo alt="Test Player" width={20} height={20} />
Test Player
</MenuItem>
<MenuItem to="/account">
<RiAccountCircleFill alt="My Account" width={20} height={20} />
My Account
</MenuItem>
{/* Scrollable navigation area */} <div className="divider">Social links</div>
<div className="overflow-y-auto grow flex flex-col"> <li>
{/* Main NavLinks */} <a
<NavLink href={externalLinks.discord}
to="/" target="_blank"
className={({ isActive }) => rel="noopener noreferrer"
`flex flex-row p-3 border-l-3 ${ >
isActive <FaDiscord alt="Discord" width={20} height={20} />
? "border-l-sky-500/50 bg-black/50" Discord
: "hover:border-l-sky-500/50 hover:bg-white/5" </a>
}` </li>
} <li>
> <a
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer"> href={externalLinks.telegram}
<img src={homeIcon} alt="Home" className="w-1/2 cursor-pointer" /> target="_blank"
</button> rel="noopener noreferrer"
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> >
Home <FaTelegram alt="Telegram" width={20} height={20} />
</p> Telegram
</NavLink> </a>
</li>
<NavLink <li>
to="/cache" <a
className={({ isActive }) => href={externalLinks.gitea}
`flex flex-row p-3 border-l-3 ${ target="_blank"
isActive rel="noopener noreferrer"
? "border-l-emerald-500/50 bg-black/50" >
: "hover:border-l-emerald-500/50 hover:bg-white/5" <SiGitea alt="Gitea" width={20} height={20} />
}` Gitea
} </a>
> </li>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer"> </ul>
<img src={cacheIcon} alt="Cache" className="w-1/2 cursor-pointer" /> </div>
</button> <a className="btn btn-ghost text-xl">CDRM-Project</a>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> </div>
Cache <div className="navbar-center hidden lg:flex">
</p> <ul className="menu menu-horizontal px-1">
</NavLink> <MenuItem to="/">
<FaHome alt="Home" width={20} height={20} />
<NavLink Home
to="/api" </MenuItem>
className={({ isActive }) => <MenuItem to="/cache">
`flex flex-row p-3 border-l-3 ${ <FaDatabase alt="Cache" width={20} height={20} />
isActive Cache
? "border-l-indigo-500/50 bg-black/50" </MenuItem>
: "hover:border-l-indigo-500/50 hover:bg-white/5" <MenuItem to="/api">
}` <IoCodeSlashSharp alt="API" width={20} height={20} />
} API
> </MenuItem>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer"> <MenuItem to="/testplayer">
<img src={apiIcon} alt="API" className="w-1/2 cursor-pointer" /> <FaVideo alt="Test Player" width={20} height={20} />
</button> Test Player
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> </MenuItem>
API <MenuItem to="/account">
</p> <RiAccountCircleFill alt="My Account" width={20} height={20} />
</NavLink>
<NavLink
to="/testplayer"
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-rose-500/50 bg-black/50"
: "hover:border-l-rose-500/50 hover:bg-white/5"
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img
src={testPlayerIcon}
alt="Test Player"
className="w-1/2 cursor-pointer"
/>
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Test Player
</p>
</NavLink>
{/* Account link at bottom of scrollable area */}
<div className="mt-auto">
<NavLink
to="/account"
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-yellow-500/50 bg-black/50"
: "hover:border-l-yellow-500/50 hover:bg-white/5"
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={accountIcon} alt="Account" className="w-1/2 cursor-pointer" />
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
My Account My Account
</p> </MenuItem>
</NavLink> </ul>
</div>
<div className="navbar-end hidden lg:flex">
<a
className="btn btn-ghost hover:text-indigo-400"
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-bottom" data-tip="CDRM Discord">
<FaDiscord className="h-6 w-6" />
</div>
</a>
<a
className="btn btn-ghost hover:text-sky-400"
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-bottom" data-tip="CDRM Telegram">
<FaTelegram className="h-6 w-6" />
</div>
</a>
<a
className="btn btn-ghost hover:text-lime-400"
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-left" data-tip="CDRM Gitea">
<SiGitea className="h-6 w-6" />
</div>
</a>
</div> </div>
</div> </div>
</>
{/* External links at very bottom */}
<div className="flex flex-row w-full h-16 bg-black/25">
<a
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
>
<img
src={discordIcon}
alt="Discord"
className="w-1/2 group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
>
<img
src={telegramIcon}
alt="Telegram"
className="w-1/2 group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group"
>
<img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
</a>
</div>
</div>
); );
} }

View File

@ -1,4 +1,8 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import NavBar from "../NavBar";
import Container from "../Container";
import { FaCopy } from "react-icons/fa";
import { toast } from "sonner";
const { protocol, hostname, port } = window.location; const { protocol, hostname, port } = window.location;
@ -10,6 +14,11 @@ if (
fullHost += `:${port}`; fullHost += `:${port}`;
} }
const handleCopy = (text) => {
navigator.clipboard.writeText(text);
toast.success("Copied to clipboard");
};
function API() { function API() {
const [deviceInfo, setDeviceInfo] = useState({ const [deviceInfo, setDeviceInfo] = useState({
device_type: "", device_type: "",
@ -61,13 +70,7 @@ function API() {
document.title = "API | CDRM-Project"; document.title = "API | CDRM-Project";
}, []); }, []);
return ( const decryptRequest = `import requests
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
<details open className="w-full list-none">
<summary className="text-2xl">Sending a decryption request</summary>
<div className="mt-5 p-5 rounded-lg border-2 border-indigo-500/50">
<pre className="rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto">
{`import requests
print(requests.post( print(requests.post(
url='${fullHost}/api/decrypt', url='${fullHost}/api/decrypt',
@ -83,55 +86,154 @@ print(requests.post(
'Accept-Language': 'en-US,en;q=0.5', 'Accept-Language': 'en-US,en;q=0.5',
}) })
} }
).json()['message'])`} ).json()['message'])`;
</pre>
</div> const searchRequest = `import requests
</details>
<details open className="w-full list-none mt-5">
<summary className="text-2xl">Sending a search request</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg">
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
{`import requests
print(requests.post( print(requests.post(
url='${fullHost}/api/cache/search', url='${fullHost}/api/cache/search',
json={ json={
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==' 'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
} }
).json())`} ).json())`;
</pre>
return (
<>
<NavBar />
<Container>
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center py-8">
<div className="join join-vertical w-full max-w-2xl">
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
Sending a decryption request
</div>
<div className="collapse-content text-slate-300">
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
{decryptRequest}
</pre>
<div className="flex justify-end">
<button
type="button"
className="btn btn-primary"
onClick={() => handleCopy(decryptRequest)}
>
<FaCopy /> Copy
</button>
</div>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
Sending a search request
</div>
<div className="collapse-content text-slate-300">
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
{searchRequest}
</pre>
<div className="flex justify-end">
<button
type="button"
className="btn btn-primary"
onClick={() => handleCopy(searchRequest)}
>
<FaCopy /> Copy
</button>
</div>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
PyWidevine RemoteCDM info
</div>
<div className="collapse-content text-slate-300">
<p>
<strong>Device Type:</strong>{" "}
<span className="font-mono">
{deviceInfo.device_type || "N/A"}
</span>
</p>
<p>
<strong>System ID:</strong>{" "}
<span className="font-mono">
{deviceInfo.system_id || "N/A"}
</span>
</p>
<p>
<strong>Security Level:</strong>{" "}
<span className="font-mono">
{deviceInfo.security_level || "N/A"}
</span>
</p>
<p>
<strong>Host:</strong>{" "}
<span className="font-mono">{fullHost}/remotecdm/widevine</span>
</p>
<p>
<strong>Secret:</strong>{" "}
<span className="font-mono">{deviceInfo.secret || "N/A"}</span>
</p>
<p>
<strong>Device Name:</strong>{" "}
<span className="font-mono">
{deviceInfo.device_name || "N/A"}
</span>
</p>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
PyPlayready RemoteCDM info
</div>
<div className="collapse-content text-slate-300">
<p>
<strong>Security Level:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.security_level || "N/A"}
</span>
</p>
<p>
<strong>Host:</strong>{" "}
<span className="font-mono">
{fullHost}/remotecdm/playready
</span>
</p>
<p>
<strong>Secret:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.secret || "N/A"}
</span>
</p>
<p>
<strong>Device Name:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.device_name || "N/A"}
</span>
</p>
</div>
</div>
</div>
</div> </div>
</details> </Container>
<details open className="w-full list-none mt-5"> </>
<summary className="text-2xl">PyWidevine RemoteCDM info</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}
<br />
<strong>Security Level:</strong> {deviceInfo.security_level}
<br />
<strong>Host:</strong> {fullHost}/remotecdm/widevine
<br />
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
<strong>Device Name:</strong> {deviceInfo.device_name}
</p>
</div>
</details>
<details open className="w-full list-none mt-5">
<summary className="text-2xl">PyPlayready RemoteCDM info</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<p>
<strong>Security Level:</strong> {prDeviceInfo.security_level}
<br />
<strong>Host:</strong> {fullHost}/remotecdm/playready
<br />
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
<strong>Device Name:</strong> {prDeviceInfo.device_name}
</p>
</div>
</details>
</div>
); );
} }

View File

@ -1,14 +1,17 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import Container from "../Container";
import NavBar from "../NavBar";
import MyAccount from "./MyAccount";
import Register from "./Register"; import Register from "./Register";
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
function Account() { function Account() {
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state const [isLoggedIn, setIsLoggedIn] = useState(null);
useEffect(() => { useEffect(() => {
fetch("/login/status", { fetch("/login/status", {
method: "POST", method: "POST",
credentials: "include", // Sends cookies with request credentials: "include",
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@ -19,19 +22,25 @@ function Account() {
} }
}) })
.catch((err) => { .catch((err) => {
toast.error(`Error checking login status. Reason: ${err.message}`);
console.error("Error checking login status:", err); console.error("Error checking login status:", err);
setIsLoggedIn(false); // Assume not logged in on error setIsLoggedIn(false);
}); });
}, []); }, []);
if (isLoggedIn === null) { if (isLoggedIn === null) {
return <div>Loading...</div>; // Optional loading UI return <div>Loading...</div>;
} }
return ( return (
<div id="accountpage" className="w-full h-full flex"> <>
{isLoggedIn ? <MyAccount /> : <Register />} <NavBar />
</div> <Container>
<div id="accountpage" className="flex h-full w-full">
{isLoggedIn ? <MyAccount /> : <Register />}
</div>
</Container>
</>
); );
} }

View File

@ -1,9 +1,16 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { FaDownload } from "react-icons/fa";
import { toast } from "sonner";
import Container from "../Container";
import NavBar from "../NavBar";
function Cache() { function Cache() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [cacheData, setCacheData] = useState([]); const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); // New state to store the key count const [keyCount, setKeyCount] = useState(0);
const [loading, setLoading] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
const debounceTimeout = useRef(null); const debounceTimeout = useRef(null);
// Fetch the key count when the component mounts // Fetch the key count when the component mounts
@ -23,32 +30,40 @@ function Cache() {
const handleInputChange = (event) => { const handleInputChange = (event) => {
const query = event.target.value; const query = event.target.value;
setSearchQuery(query); // Update the search query setSearchQuery(query);
// Clear the previous timeout
if (debounceTimeout.current) { if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current); clearTimeout(debounceTimeout.current);
} }
// Set a new timeout to send the API call after 1 second of no typing if (query.trim() !== "") {
debounceTimeout.current = setTimeout(() => { setLoading(true); // Show spinner immediately
if (query.trim() !== "") { debounceTimeout.current = setTimeout(() => {
sendApiCall(query); // Only call the API if the query is not empty sendApiCall(query);
} else { }, 1000);
setCacheData([]); // Clear results if query is empty } else {
} setHasSearched(false); // Reset state when input is cleared
}, 1000); // 1 second delay setCacheData([]);
}
}; };
const sendApiCall = (text) => { const sendApiCall = (text) => {
setLoading(true);
fetch("/api/cache/search", { fetch("/api/cache/search", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: text }), body: JSON.stringify({ input: text }),
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setCacheData(data)) // Update cache data with the results .then((data) => {
.catch((error) => console.error("Error:", error)); setCacheData(data);
setHasSearched(true);
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
console.error("Error:", error);
})
.finally(() => setLoading(false));
}; };
useEffect(() => { useEffect(() => {
@ -56,51 +71,71 @@ function Cache() {
}, []); }, []);
return ( return (
<div className="flex flex-col w-full h-full overflow-y-auto p-4"> <>
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center"> <NavBar />
<input <Container>
type="text" <div className="my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
value={searchQuery} <fieldset className="fieldset w-full max-w-2xl">
onChange={handleInputChange} <input
placeholder={`Search ${keyCount} keys...`} // Dynamic placeholder type="text"
className="lg:grow w-full border-2 border-emerald-500/25 rounded-xl h-10 self-center m-2 text-white p-1 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all duration-200 ease-in-out" value={searchQuery}
/> onChange={handleInputChange}
<a placeholder={`Search ${keyCount} keys...`}
href="/api/cache/download" className="input w-full max-w-2xl font-mono"
className="bg-emerald-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-10 truncate w-full text-center flex items-center justify-center m-2" />
> </fieldset>
Download Cache <button
</a> className="btn btn-success"
</div> onClick={() => {
<div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto"> window.location.href = "/api/cache/download";
<table className="min-w-full text-white"> }}
<thead> >
<tr> <FaDownload />
<th className="p-2 border border-black">PSSH</th> Download keys cache
<th className="p-2 border border-black">KID</th> </button>
<th className="p-2 border border-black">Key</th> </div>
</tr>
</thead> {loading ? (
<tbody> <div className="flex justify-center py-16">
{cacheData.length > 0 ? ( <span className="loading loading-spinner loading-md me-2"></span>
cacheData.map((item, index) => ( Searching...
<tr key={index}> </div>
<td className="p-2 border border-black">{item.PSSH}</td> ) : cacheData.length > 0 ? (
<td className="p-2 border border-black">{item.KID}</td> <div className="my-4 flex justify-center">
<td className="p-2 border border-black">{item.Key}</td> <div className="overflow-x-auto">
</tr> <table className="table">
)) <thead>
) : ( <tr>
<tr> <th></th>
<td colSpan="3" className="p-2 border border-black text-center"> <th className="text-center">PSSH</th>
No data found <th className="text-center">key ID:key pair</th>
</td> </tr>
</tr> </thead>
)} <tbody>
</tbody> {cacheData.map((item, index) => (
</table> <tr key={index}>
</div> <th>{index + 1}</th>
</div> <td className="font-mono">{item.PSSH}</td>
<td className="font-mono">
{item.KID}:{item.Key}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : hasSearched ? (
<div className="flex justify-center py-16">
<div className="text-center">No data found in the database</div>
</div>
) : (
<div className="flex justify-center py-16">
<div className="text-center">Enter a search query to see results</div>
</div>
)}
</Container>
</>
); );
} }

View File

@ -1,5 +1,9 @@
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { readTextFromClipboard } from "../Functions/ParseChallenge"; import { readTextFromClipboard } from "../Functions/ParseChallenge";
import NavBar from "../NavBar";
import Container from "../Container";
import { toast } from "sonner";
import { IoInformationCircleOutline } from "react-icons/io5";
function HomePage() { function HomePage() {
const [pssh, setPssh] = useState(""); const [pssh, setPssh] = useState("");
@ -58,7 +62,7 @@ function HomePage() {
}) })
.catch((error) => { .catch((error) => {
console.error("Error during decryption request:", error); console.error("Error during decryption request:", error);
setMessage("Error: Unable to process request."); setMessage(`Error: Unable to process request. Reason: ${error.message}`);
setIsVisible(true); setIsVisible(true);
}); });
}; };
@ -67,14 +71,15 @@ function HomePage() {
event.preventDefault(); event.preventDefault();
if (messageRef.current) { if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks) const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
toast.success("Copied to clipboard");
navigator.clipboard.writeText(textToCopy).catch((err) => { navigator.clipboard.writeText(textToCopy).catch((err) => {
alert("Failed to copy!"); toast.error(`Failed to copy. Reason: ${err.message}`);
console.error(err); console.error(err);
}); });
} }
}; };
const handleFetchPaste = () => { const handleFetchPaste = (event) => {
event.preventDefault(); event.preventDefault();
readTextFromClipboard() readTextFromClipboard()
.then(() => { .then(() => {
@ -84,7 +89,8 @@ function HomePage() {
setData(document.getElementById("data").value); setData(document.getElementById("data").value);
}) })
.catch((err) => { .catch((err) => {
alert("Failed to paste from fetch!"); toast.error(`Failed to paste from fetch. Reason: ${err.message}`);
console.error("Failed to paste from fetch:", err);
}); });
}; };
@ -133,138 +139,150 @@ function HomePage() {
return ( return (
<> <>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full"> <NavBar />
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto"> <Container>
<label htmlFor="pssh" className="text-white w-8/10 self-center"> <div className="mx-auto flex w-full max-w-2xl flex-col justify-center">
PSSH:{" "} <fieldset className="fieldset">
</label> <legend className="fieldset-legend text-base">PSSH*</legend>
<input <input
type="text" type="text"
id="pssh" className="input w-full font-mono"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1" placeholder="Enter PSSH here"
value={pssh} value={pssh}
onChange={(e) => setPssh(e.target.value)} onChange={(e) => setPssh(e.target.value)}
/> required
<label htmlFor="licurl" className="text-white w-8/10 self-center"> />
License URL:{" "} <p className="label text-red-500">* Required</p>
</label> </fieldset>
<input <fieldset className="fieldset">
type="text" <legend className="fieldset-legend text-base">License URL*</legend>
id="licurl" <input
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1" type="text"
value={licurl} className="input w-full font-mono"
onChange={(e) => setLicurl(e.target.value)} placeholder="Enter License URL here"
/> value={licurl}
<label htmlFor="proxy" className="text-white w-8/10 self-center"> onChange={(e) => setLicurl(e.target.value)}
Proxy:{" "} required
</label> />
<input <p className="label text-red-500">* Required</p>
type="text" </fieldset>
id="proxy" <fieldset className="fieldset">
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1" <legend className="fieldset-legend text-base">Proxy</legend>
value={proxy} <input
onChange={(e) => setProxy(e.target.value)} type="text"
/> className="input w-full font-mono"
<label htmlFor="headers" className="text-white w-8/10 self-center"> placeholder="Enter Proxy here (https://example.com:8080)"
Headers:{" "} value={proxy}
</label> onChange={(e) => setProxy(e.target.value)}
<textarea />
id="headers" </fieldset>
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48" <fieldset className="fieldset">
value={headers} <legend className="fieldset-legend text-base">
onChange={(e) => setHeaders(e.target.value)} Headers*
/> <div
<label htmlFor="cookies" className="text-white w-8/10 self-center"> className="tooltip"
Cookies:{" "} data-tip="You can use https://curlconverter.com/python/ to paste the header values here"
</label> >
<textarea <IoInformationCircleOutline className="h-5 w-5" />
id="cookies" </div>
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48" </legend>
value={cookies} <textarea
onChange={(e) => setCookies(e.target.value)} className="textarea h-48 w-full font-mono"
/> placeholder="Enter headers here (JSON format). E.g. {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}"
<label htmlFor="data" className="text-white w-8/10 self-center"> value={headers}
Data:{" "} onChange={(e) => setHeaders(e.target.value)}
</label> required
<textarea />
id="data" <p className="label text-red-500">* Required</p>
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48" </fieldset>
value={data} <fieldset className="fieldset">
onChange={(e) => setData(e.target.value)} <legend className="fieldset-legend text-base">Cookies</legend>
/> <textarea
className="textarea h-48 w-full font-mono"
placeholder="Enter cookies here (JSON format)"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Data</legend>
<textarea
className="textarea h-48 w-full font-mono"
placeholder="Enter data here (JSON format)"
value={data}
onChange={(e) => setData(e.target.value)}
/>
</fieldset>
{/* Device Selection Dropdown, only show if logged in */} {/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && ( {devices.length > 0 && (
<> <>
<label htmlFor="device" className="text-white w-8/10 self-center"> <fieldset className="fieldset">
Select Device: <legend className="fieldset-legend text-base">Select device</legend>
</label> <select
<select className="select w-full"
id="device" value={selectedDevice}
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1" onChange={(e) => setSelectedDevice(e.target.value)}
value={selectedDevice} >
onChange={(e) => setSelectedDevice(e.target.value)} {devices.map((device, index) => (
> <option key={index} value={device}>
{devices.map((device, index) => ( {device}
<option key={index} value={device}> </option>
{device} ))}
</option> </select>
))} </fieldset>
</select>
</> </>
)} )}
<div className="mx-auto my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button <button
type="button" type="button"
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2" className="btn btn-primary btn-wide"
onClick={handleSubmitButton} onClick={handleSubmitButton}
disabled={pssh === "" || licurl === "" || headers === ""}
> >
Submit Submit
</button> </button>
<button <button
type="button"
className="btn btn-info btn-wide"
onClick={handleFetchPaste} onClick={handleFetchPaste}
className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
> >
Paste from fetch Paste from fetch
</button> </button>
<button <button
type="button" type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0" className="btn btn-error btn-wide"
onClick={handleReset} onClick={handleReset}
> >
Reset Reset
</button> </button>
</div> </div>
</form>
</div>
{isVisible && (
<div
id="main_content"
className="flex-col w-full h-full p-10 items-center justify-center self-center"
>
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<div className="w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5">
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
<p
className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
</div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy}
>
Copy Results
</button>
</div>
</div> </div>
)}
{isVisible && (
<>
<div className="mx-auto my-4 flex w-full max-w-2xl flex-col justify-center">
<div className="card bg-base-100 card-lg border border-gray-500 shadow-sm">
<div className="card-body">
<h2 className="card-title">Result</h2>
<div className="divider"></div>
<p
className="w-full grow overflow-y-auto font-mono break-words"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
<div
className="card-actions mt-4 justify-end"
onClick={handleCopy}
>
<button className="btn btn-success">Copy results</button>
</div>
</div>
</div>
</div>
</>
)}
</Container>
</> </>
); );
} }

View File

@ -1,5 +1,7 @@
import axios from "axios"; import axios from "axios";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import Container from "../Container";
function MyAccount() { function MyAccount() {
const [wvList, setWvList] = useState([]); const [wvList, setWvList] = useState([]);
@ -21,6 +23,7 @@ function MyAccount() {
setUsername(response.data.Styled_Username || ""); setUsername(response.data.Styled_Username || "");
setApiKey(response.data.API_Key || ""); setApiKey(response.data.API_Key || "");
} catch (err) { } catch (err) {
toast.error(`Failed to fetch user info. Reason: ${err.message}`);
console.error("Failed to fetch user info", err); console.error("Failed to fetch user info", err);
} }
}; };
@ -43,7 +46,7 @@ function MyAccount() {
(cdmType === "PR" && extension !== "prd") || (cdmType === "PR" && extension !== "prd") ||
(cdmType === "WV" && extension !== "wvd") (cdmType === "WV" && extension !== "wvd")
) { ) {
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`); toast.error(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return; return;
} }
@ -55,9 +58,10 @@ function MyAccount() {
await axios.post(`/upload/${cdmType}`, formData); await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload await fetchUserInfo(); // Refresh list after upload
} catch (err) { } catch (err) {
toast.error(`Upload failed. Reason: ${err.message}`);
console.error("Upload failed", err); console.error("Upload failed", err);
alert("Upload failed");
} finally { } finally {
toast.success(`${cdmType} CDM uploaded successfully`);
setUploading(false); setUploading(false);
} }
}; };
@ -66,17 +70,18 @@ function MyAccount() {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post("/logout"); await axios.post("/logout");
toast.success("Logged out successfully. Reloading page...");
window.location.reload(); window.location.reload();
} catch (error) { } catch (error) {
toast.error(`Logout failed. Reason: ${error.message}`);
console.error("Logout failed:", error); console.error("Logout failed:", error);
alert("Logout failed!");
} }
}; };
// Handle change password // Handle change password
const handleChangePassword = async () => { const handleChangePassword = async () => {
if (passwordError || password === "") { if (passwordError || password === "") {
alert("Please enter a valid password."); toast.error("Please enter a valid password");
return; return;
} }
@ -86,16 +91,16 @@ function MyAccount() {
}); });
if (response.data.message === "True") { if (response.data.message === "True") {
alert("Password changed successfully."); toast.success("Password changed successfully");
setPassword(""); setPassword("");
} else { } else {
alert("Failed to change password."); toast.error("Failed to change password");
} }
} catch (error) { } catch (error) {
if (error.response && error.response.data?.message === "Invalid password format") { if (error.response && error.response.data?.message === "Invalid password format") {
alert("Password format is invalid. Please try again."); toast.error("Password format is invalid. Please try again.");
} else { } else {
alert("Error occurred while changing password."); toast.error("Error occurred while changing password");
} }
} }
}; };
@ -103,7 +108,7 @@ function MyAccount() {
// Handle change API key // Handle change API key
const handleChangeApiKey = async () => { const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === "") { if (apiKeyError || newApiKey === "") {
alert("Please enter a valid API key."); toast.error("Please enter a valid API key");
return; return;
} }
@ -112,181 +117,201 @@ function MyAccount() {
new_api_key: newApiKey, new_api_key: newApiKey,
}); });
if (response.data.message === "True") { if (response.data.message === "True") {
alert("API key changed successfully."); toast.success("API key changed successfully");
setApiKey(newApiKey); setApiKey(newApiKey);
setNewApiKey(""); setNewApiKey("");
} else { } else {
alert("Failed to change API key."); toast.error("Failed to change API key");
} }
} catch (error) { } catch (error) {
alert("Error occurred while changing API key."); toast.error("Error occurred while changing API key");
console.error(error); console.error(error);
} }
}; };
return ( return (
<div <>
id="myaccount" <Container>
className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4" <div className="flex flex-col gap-4 p-4 lg:flex-row">
> {/* Left Panel - Account Settings */}
<div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto"> <div className="w-full lg:w-96">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2"> <div className="card bg-base-200 shadow-xl">
{username ? `${username}` : "My Account"} <div className="card-body">
</h1> <p className="text-center text-sm">Username:</p>
<h2 className="card-title justify-center text-center font-bold">
{username}
</h2>
{/* API Key Section */} <div className="divider"></div>
<div className="w-full flex flex-col items-center">
<label htmlFor="apiKey" className="text-white font-semibold mb-1">
API Key
</label>
<input
id="apiKey"
type="text"
value={apiKey}
readOnly
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{/* New API Key Section */} <fieldset className="fieldset">
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1"> <legend className="fieldset-legend text-base" htmlFor="apiKey">
New API Key API Key
</label> </legend>
<input <input
id="newApiKey" name="apiKey"
type="text" type="text"
value={newApiKey} value={apiKey}
onChange={(e) => { readOnly
const value = e.target.value; className="input input-bordered text-center"
const isValid = /^[^\s]+$/.test(value); // No spaces />
if (!isValid) {
setApiKeyError("API key must not contain spaces.");
} else {
setApiKeyError("");
}
setNewApiKey(value);
}}
placeholder="Enter new API key"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangeApiKey}
>
Change API Key
</button>
{/* Change Password Section */} <legend
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1"> className="fieldset-legend text-base"
Change Password htmlFor="newApiKey"
</label> >
<input New API Key
id="password" </legend>
type="password" <input
value={password} name="newApiKey"
onChange={(e) => { type="text"
const value = e.target.value; value={newApiKey}
const isValid = onChange={(e) => {
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value); const value = e.target.value;
if (!isValid) { const isValid = /^[^\s]+$/.test(value);
setPasswordError( if (!isValid) {
"Password must not contain spaces or invalid characters." setApiKeyError("API key must not contain spaces");
); } else {
} else { setApiKeyError("");
setPasswordError(""); }
} setNewApiKey(value);
setPassword(value); }}
}} placeholder="Enter new API key"
placeholder="New Password" className="input input-bordered"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center" />
/> {apiKeyError && (
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>} <p className="label text-error">{apiKeyError}</p>
<button )}
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white" <button
onClick={handleChangePassword} className="btn btn-primary btn-block mt-2"
> onClick={handleChangeApiKey}
Change Password >
</button> Change API key
</div> </button>
</fieldset>
<button <fieldset className="fieldset">
onClick={handleLogout} <legend
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white" className="fieldset-legend text-base"
> htmlFor="passwordChange"
Log out >
</button> Change password
</div> </legend>
<input
name="passwordChange"
type="password"
value={password}
onChange={(e) => {
const value = e.target.value;
const isValid =
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(
value
);
if (!isValid) {
setPasswordError("Invalid password characters");
} else {
setPasswordError("");
}
setPassword(value);
}}
placeholder="New password"
className="input input-bordered"
/>
{passwordError && (
<p className="label text-error">{passwordError}</p>
)}
<button
className="btn btn-secondary btn-block mt-2"
onClick={handleChangePassword}
>
Change password
</button>
</fieldset>
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0"> <div className="divider"></div>
{/* Widevine Section */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto"> <button className="btn btn-error mt-auto" onClick={handleLogout}>
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2"> Log out
Widevine CDMs </button>
</h1>
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
{wvList.length === 0 ? (
<div className="text-white text-center font-bold">
No Widevine CDMs uploaded.
</div> </div>
) : ( </div>
wvList.map((filename, i) => (
<div
key={i}
className={`text-center font-bold text-white p-2 rounded ${
i % 2 === 0 ? "bg-black/30" : "bg-black/60"
}`}
>
{filename}
</div>
))
)}
</div> </div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, "WV")}
/>
</label>
</div>
{/* Playready Section */} {/* Right Panel - CDM Uploads */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto"> <div className="flex w-full flex-col gap-4">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black"> {/* Widevine CDM */}
Playready CDMs <div className="card bg-base-200 shadow-xl">
</h1> <div className="card-body">
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2"> <h2 className="card-title">Widevine CDMs</h2>
{prList.length === 0 ? ( <div className="divider"></div>
<div className="text-white text-center font-bold"> <div className="max-h-60 space-y-2 overflow-y-auto">
No Playready CDMs uploaded. {wvList.length === 0 ? (
</div> <div className="text-center text-sm">
) : ( No Widevine CDMs uploaded.
prList.map((filename, i) => ( </div>
<div ) : (
key={i} wvList.map((filename, i) => (
className={`text-center font-bold text-white p-2 rounded ${ <div
i % 2 === 0 ? "bg-black/30" : "bg-black/60" key={i}
}`} className={`rounded px-2 py-1 text-sm ${
> i % 2 === 0 ? "bg-base-100" : "bg-base-300"
{filename} }`}
>
{filename}
</div>
))
)}
</div> </div>
)) <label className="btn btn-accent mt-4">
)} {uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, "WV")}
/>
</label>
</div>
</div>
{/* PlayReady CDM */}
<div className="card bg-base-200 shadow-xl">
<div className="card-body">
<h2 className="card-title">PlayReady CDMs</h2>
<div className="divider"></div>
<div className="max-h-60 space-y-2 overflow-y-auto">
{prList.length === 0 ? (
<div className="text-center text-sm">
No PlayReady CDMs uploaded.
</div>
) : (
prList.map((filename, i) => (
<div
key={i}
className={`rounded px-2 py-1 text-sm ${
i % 2 === 0 ? "bg-base-100" : "bg-base-300"
}`}
>
{filename}
</div>
))
)}
</div>
<label className="btn btn-accent mt-4">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, "PR")}
/>
</label>
</div>
</div>
</div> </div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, "PR")}
/>
</label>
</div> </div>
</div> </Container>
</div> </>
); );
} }

View File

@ -1,118 +1,146 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { IoIosLogIn } from "react-icons/io";
import { PiUserCirclePlus } from "react-icons/pi";
function Register() { function Register() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [status, setStatus] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [tab, setTab] = useState("login"); // 'login' or 'register'
// Validation functions
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
useEffect(() => { useEffect(() => {
document.title = "Register | CDRM-Project"; document.title = "Register | CDRM-Project";
}, []); }, []);
const handleRegister = async () => { const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateUsername(username)) { if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores."); toast.error("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return; return;
} }
if (!validatePassword(password)) { if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed."); toast.error("Invalid password. Spaces are not allowed.");
return; return;
} }
try { if (tab === "register") {
const response = await fetch("/register", { if (password !== confirmPassword) {
method: "POST", toast.error("Passwords do not match.");
headers: { return;
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.message) {
setStatus(data.message);
} else if (data.error) {
setStatus(data.error);
} }
} catch (err) { try {
setStatus("An error occurred while registering."); const res = await fetch("/register", {
} method: "POST",
}; headers: {
"Content-Type": "application/json",
const handleLogin = async () => { },
if (!validateUsername(username)) { body: JSON.stringify({ username, password }),
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores."); });
return; const data = await res.json();
} if (data.message) {
if (!validatePassword(password)) { toast.success(data.message);
setStatus("Invalid password. Spaces are not allowed."); } else {
return; toast.error(data.error || "Unknown error");
} }
} catch (err) {
try { toast.error(`Register error: ${err.message}`);
const response = await fetch("/login", { }
method: "POST", } else {
headers: { try {
"Content-Type": "application/json", const res = await fetch("/login", {
}, method: "POST",
credentials: "include", // Important to send cookies headers: {
body: JSON.stringify({ username, password }), "Content-Type": "application/json",
}); },
const data = await response.json(); credentials: "include",
if (data.message) { body: JSON.stringify({ username, password }),
// Successful login - reload the page to trigger Account check });
window.location.reload(); const data = await res.json();
} else if (data.error) { if (data.message) {
setStatus(data.error); window.location.reload();
} else {
toast.error(data.error || "Login failed");
}
} catch (err) {
toast.error(`Login error: ${err.message}`);
} }
} catch (err) {
setStatus("An error occurred while logging in.");
} }
}; };
return ( return (
<div className="flex flex-col w-full h-full items-center justify-center p-4"> <div className="mx-auto flex min-h-full w-full max-w-xl flex-col justify-center px-6 py-12 lg:px-8">
<div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center"> <div className="mx-auto">
<div className="flex flex-col w-full"> {/* Tabs */}
<label htmlFor="username" className="text-lg font-bold mb-2 text-white"> <div className="tabs tabs-box justify-center">
Username:
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">
Password:
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
</div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
<button <button
onClick={handleLogin} className={`tab ${tab === "login" ? "tab-active" : ""}`}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3" onClick={() => setTab("login")}
> >
Login <IoIosLogIn className="h-6 w-6 me-1"/>
Sign in
</button> </button>
<button <button
onClick={handleRegister} className={`tab ${tab === "register" ? "tab-active" : ""}`}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3" onClick={() => setTab("register")}
> >
<PiUserCirclePlus className="h-6 w-6 me-1" />
Register Register
</button> </button>
</div> </div>
{status && <p className="text-sm text-white mt-4 p-4">{status}</p>} <h2 className="mt-10 text-center text-2xl font-bold tracking-tight">
{tab === "login" ? "Sign in" : "Register"}
</h2>
</div>
<div className="mx-auto mt-10 w-full max-w-xl">
<form className="space-y-6" onSubmit={handleSubmit}>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Username</legend>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="input w-full"
placeholder="Enter your username"
required
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Password</legend>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input w-full"
placeholder="Enter your password"
required
/>
</fieldset>
{tab === "register" && (
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Confirm password</legend>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input w-full"
placeholder="Confirm your password"
required
/>
</fieldset>
)}
<button type="submit" className="btn btn-primary btn-block">
{tab === "login" ? "Sign in" : "Register"}
</button>
</form>
</div> </div>
</div> </div>
); );

View File

@ -1,27 +1,23 @@
import React, { useState, useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import shaka from "shaka-player"; import shaka from "shaka-player";
import { toast } from "sonner";
import Container from "../Container";
import NavBar from "../NavBar";
function TestPlayer() { function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL const [mpdUrl, setMpdUrl] = useState("");
const [kids, setKids] = useState(""); // State to hold KIDs (separated by line breaks) const [headers, setHeaders] = useState("");
const [keys, setKeys] = useState(""); // State to hold Keys (separated by line breaks) const [keyPairs, setKeyPairs] = useState(""); // "kid:key" per line
const [headers, setHeaders] = useState(""); // State to hold request headers
const videoRef = useRef(null); // Ref for the video element const videoRef = useRef(null);
const playerRef = useRef(null); // Ref for Shaka Player instance const playerRef = useRef(null);
// Function to update the MPD URL state
const handleInputChange = (event) => { const handleInputChange = (event) => {
setMpdUrl(event.target.value); setMpdUrl(event.target.value);
}; };
// Function to update KIDs and Keys const handleKeyPairsChange = (event) => {
const handleKidsChange = (event) => { setKeyPairs(event.target.value);
setKids(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
}; };
const handleHeadersChange = (event) => { const handleHeadersChange = (event) => {
@ -29,31 +25,37 @@ function TestPlayer() {
}; };
// Function to initialize Shaka Player // Function to initialize Shaka Player
const initializePlayer = () => { const initializePlayer = async () => {
if (videoRef.current) { if (videoRef.current && !playerRef.current) {
// Initialize Shaka Player only if it's not already initialized const player = new shaka.Player(); // no mediaElement
if (!playerRef.current) { await player.attach(videoRef.current); // attach later
const player = new shaka.Player(videoRef.current); playerRef.current = player;
playerRef.current = player;
// Add error listener player.addEventListener("error", (event) => {
player.addEventListener("error", (event) => { toast.error(`Error code ${event.detail.code}: ${event.detail.message}`);
console.error("Error code", event.detail.code, "object", event.detail); console.error("Error code", event.detail.code, "object", event.detail);
}); });
}
} }
}; };
// Function to handle submit and configure player with DRM keys and headers // Function to handle submit and configure player with DRM keys and headers
const handleSubmit = () => { const handleSubmit = () => {
if (mpdUrl && kids && keys) { if (mpdUrl && keyPairs) {
// Split the KIDs and Keys by new lines // Parse KID:KEY pairs
const kidsArray = kids.split("\n").map((k) => k.trim()); const lines = keyPairs
const keysArray = keys.split("\n").map((k) => k.trim()); .split("\n")
.map((line) => line.trim())
.filter(Boolean);
const clearKeys = {};
if (kidsArray.length !== keysArray.length) { for (const line of lines) {
console.error("The number of KIDs and Keys must be the same."); const [kid, key] = line.split(":").map((part) => part.trim());
return; if (!kid || !key) {
toast.error(`Invalid line (expected format keyId:key) at line "${line}"`);
console.error(`Invalid line (expected format keyId:key) at line "${line}"`);
return;
}
clearKeys[kid] = key;
} }
// Initialize Shaka Player only when the submit button is pressed // Initialize Shaka Player only when the submit button is pressed
@ -62,15 +64,10 @@ function TestPlayer() {
// Widevine DRM configuration with the provided KIDs and Keys // Widevine DRM configuration with the provided KIDs and Keys
const config = { const config = {
drm: { drm: {
clearKeys: {}, clearKeys: clearKeys,
}, },
}; };
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config and headers:", config); console.log("Configuring player with the following DRM config and headers:", config);
// Configure the player with ClearKey DRM and custom headers // Configure the player with ClearKey DRM and custom headers
@ -81,12 +78,15 @@ function TestPlayer() {
.load(mpdUrl) .load(mpdUrl)
.then(() => { .then(() => {
console.log("Video loaded"); console.log("Video loaded");
toast.success("Video successfully loaded");
}) })
.catch((error) => { .catch((error) => {
toast.error(`Error loading the video. Reason: ${error.message}`);
console.error("Error loading the video", error); console.error("Error loading the video", error);
}); });
} else { } else {
console.error("MPD URL, KIDs, and Keys are required."); toast.error("Manifest URL and key pairs are required");
console.error("Manifest URL and key pairs are required.");
} }
}; };
@ -113,39 +113,65 @@ function TestPlayer() {
}, []); }, []);
return ( return (
<div className="flex flex-col items-center w-full p-4"> <>
<div className="w-full flex flex-col"> <NavBar />
<video ref={videoRef} width="100%" height="auto" controls className="h-96" /> <Container>
<input <div className="flex w-full flex-col items-center justify-center py-8">
type="text" <div className="flex w-full flex-col items-center lg:flex-row lg:items-start lg:gap-4">
value={mpdUrl} {/* Video Section */}
onChange={handleInputChange} <div className="w-full lg:w-1/2">
placeholder="MPD URL" <video
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200" ref={videoRef}
/> width="100%"
<textarea height="auto"
placeholder="KIDs (one per line)" controls
value={kids} className="aspect-video max-h-96 w-full"
onChange={handleKidsChange} />
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200" </div>
/>
<textarea {/* Inputs Section */}
placeholder="Keys (one per line)" <div className="mt-4 flex w-full flex-col items-center lg:mt-0 lg:w-1/2">
value={keys} <fieldset className="fieldset w-full">
onChange={handleKeysChange} <legend className="fieldset-legend text-base">Manifest URL*</legend>
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200" <input
/> type="text"
<textarea value={mpdUrl}
placeholder="Headers (one per line)" onChange={handleInputChange}
value={headers} placeholder="Enter manifest URL here"
onChange={handleHeadersChange} className="input w-full font-mono"
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200" />
/> <p className="label text-red-500">* Required</p>
<button onClick={handleSubmit} className="mt-4 p-2 bg-blue-500 text-white rounded"> </fieldset>
Submit <fieldset className="fieldset w-full">
</button> <legend className="fieldset-legend text-base">Key pairs*</legend>
</div> <textarea
</div> placeholder="keyId:key pair (one per line)"
value={keyPairs}
onChange={handleKeyPairsChange}
className="textarea w-full font-mono"
/>
<p className="label text-red-500">* Required</p>
</fieldset>
<fieldset className="fieldset w-full">
<legend className="fieldset-legend text-base">Headers</legend>
<textarea
placeholder="Headers (one per line)"
value={headers}
onChange={handleHeadersChange}
className="textarea w-full font-mono"
/>
</fieldset>
<button
onClick={handleSubmit}
className="btn btn-primary btn-wide my-4"
>
Submit
</button>
</div>
</div>
</div>
</Container>
</>
); );
} }

View File

@ -1,4 +1,31 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "dim";
default: true;
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--depth: 0;
--noise: 0;
}
:root {
--font-sans:
"Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
font-family: var(--font-sans);
}
/* Force Sonner toast to use Inter first */
[data-sonner-toast],
.sonner-toast,
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
font-family: var(--font-sans) !important;
}
details summary::-webkit-details-marker { details summary::-webkit-details-marker {
display: none; display: none;

View File

@ -1,13 +1,22 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import "./index.css"; import { Toaster } from "sonner";
import App from "./App.jsx"; import App from "./App.jsx";
import "./assets/fonts/font-face.css";
import "./index.css";
createRoot(document.getElementById("root")).render( createRoot(document.getElementById("root")).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
<Toaster
richColors
className="flex justify-center"
position="bottom-center"
duration="7000"
theme="dark"
/>
</BrowserRouter> </BrowserRouter>
</StrictMode> </StrictMode>
); );