Former-commit-id: cea35bb1dcea9b93f55cd6ffef2532638b89afb0 [formerly 2ccaba0d145da0dad36b35fa36ffe83379d04f33] [formerly 251a201c53525d768dc2f620dd5ef7e3424a624a [formerly f3a08b9ad7ecb24d8f4e98e97b684d3c9460ce9d]]
Former-commit-id: 7c0834702e2640523a5cae9b61f6eaccf60a1f2b [formerly 9b1217b70f540da999ff0cf5ca9ff6bcdc21aca6]
Former-commit-id: 3c12c07b25b0b9ccdada4caf2d73c7f384b71c8e
This commit is contained in:
melvinchia3636
2024-02-01 16:35:52 +08:00
parent dcda5582ac
commit ba607fca51
119 changed files with 0 additions and 20993 deletions

View File

@@ -1,54 +0,0 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:tailwindcss/recommended"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"import",
"tailwindcss"
],
"rules": {
"@typescript-eslint/space-before-function-paren": "off",
},
"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx",
".d.ts"
]
},
},
"react": {
"version": "detect"
}
}
}

26
.gitignore vendored
View File

@@ -1,26 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
database/

View File

@@ -1,7 +0,0 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@@ -1,30 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en" d>
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' aria-hidden='true' role='img' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgb(20 184 166)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m11.414 10l-7.383 7.418a2.091 2.091 0 0 0 0 2.967a2.11 2.11 0 0 0 2.976 0L14.414 13m3.707 2.293l2.586-2.586a1 1 0 0 0 0-1.414l-7.586-7.586a1 1 0 0 0-1.414 0L9.121 6.293a1 1 0 0 0 0 1.414l7.586 7.586a1 1 0 0 0 1.414 0z'%3E%3C/path%3E%3C/svg%3E"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LifeForge.</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,72 +0,0 @@
{
"name": "personal-management-system",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@headlessui/react": "^1.7.17",
"@iconify/collections": "^1.0.375",
"@iconify/react": "^4.1.1",
"@types/moment": "^2.13.0",
"@types/react-custom-scrollbars": "^4.0.12",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/codemirror-theme-atomone": "^4.21.21",
"@uiw/codemirror-theme-material": "^4.21.21",
"@uiw/codemirror-theme-nord": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
"@uiw/react-color": "^2.0.5",
"autoprefixer": "^10.4.16",
"axios": "^1.6.7",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"daisyui": "^4.4.14",
"eslint-plugin-tailwindcss": "^3.13.0",
"express": "^4.18.2",
"moment": "^2.30.1",
"pocketbase": "^0.19.0",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-activity-calendar": "^2.2.0",
"react-chartjs-2": "^5.2.0",
"react-color": "^2.19.3",
"react-columns": "^1.2.1",
"react-custom-scrollbars-2": "^4.5.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-helmet": "^6.1.0",
"react-medium-image-zoom": "^5.1.9",
"react-router": "^6.20.0",
"react-router-dom": "^6.20.0",
"react-toastify": "^9.1.3",
"react-tooltip": "^5.24.0",
"tailwindcss": "^3.3.5"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@types/": "uidotdev/usehooks",
"@types/react": "^18.2.37",
"@types/react-color": "^3.0.10",
"@types/react-dom": "^18.2.15",
"@types/react-dropzone": "^5.1.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.0.1",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"typescript": "*",
"vite": "^5.0.0"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -1,40 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import AppRouter from './Router'
import GlobalStateProvider from './providers/GlobalStateProvider'
import { ToastContainer } from 'react-toastify'
import AuthProvider from './providers/AuthProvider'
import PersonalizationProvider from './providers/PersonalizationProvider'
function App(): React.JSX.Element {
return (
<BrowserRouter>
<GlobalStateProvider>
<AuthProvider>
<PersonalizationProvider>
<div className="relative flex h-[100dvh] w-full overflow-hidden bg-neutral-200/50 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-100">
<AppRouter />
</div>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={true}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="colored"
/>
</PersonalizationProvider>
</AuthProvider>
</GlobalStateProvider>
</BrowserRouter>
)
}
export default App

View File

@@ -1,18 +0,0 @@
import React from 'react'
import Sidebar from './components/Sidebar'
import Header from './components/Header'
import { Outlet } from 'react-router'
function MainApplication(): React.JSX.Element {
return (
<>
<Sidebar />
<main className="flex h-full w-full min-w-0 flex-col pb-0 sm:ml-20 lg:ml-0">
<Header />
<Outlet />
</main>
</>
)
}
export default MainApplication

View File

@@ -1,88 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import React, { useContext, useEffect } from 'react'
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import Dashboard from './modules/Dashboard'
import Auth from './auth'
import MainApplication from './MainApplication'
import { AuthContext } from './providers/AuthProvider'
import TodoList from './modules/TodoList'
import Calendar from './modules/Calendar'
import Projects from './modules/Projects'
import Kanban from './modules/Projects/components/Kanban'
import NotFound from './components/general/NotFound'
import IdeaBox from './modules/IdeaBox'
import Ideas from './modules/IdeaBox/components/Ideas'
import Snippets from './modules/Snippets'
import Resources from './modules/Resources'
import CodeTime from './modules/CodeTime'
import PomodoroTimer from './modules/PomodoroTimer'
import Flashcards from './modules/Flashcards'
import CardSet from './modules/Flashcards/components/CardSet'
import ReferenceBooks from './modules/ReferenceBooks'
import UniversityAnalytics from './modules/UniversityAnalytics'
import Changelog from './modules/Changelog'
import Personalization from './modules/Personalization'
import Notes from './modules/Notes'
import NotesCategory from './modules/Notes/Workspace'
import NotesSubject from './modules/Notes/Subject'
function AppRouter(): React.JSX.Element {
const { auth, authLoading } = useContext(AuthContext)
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
if (!authLoading) {
if (!auth && location.pathname !== '/auth') {
navigate('/auth?redirect=' + location.pathname)
} else if (auth) {
if (location.pathname === '/auth') {
if (location.search) {
const redirect = new URLSearchParams(location.search).get(
'redirect'
)
if (redirect) {
navigate(redirect)
} else {
navigate('/dashboard')
}
}
} else if (location.pathname === '/') {
navigate('/dashboard')
}
}
}
}, [auth, location, authLoading])
return (
<Routes>
<Route path="/" element={<MainApplication />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="todo-list" element={<TodoList />} />
<Route path="calendar" element={<Calendar />} />
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<Kanban />} />
<Route path="idea-box" element={<IdeaBox />} />
<Route path="idea-box/:id" element={<Ideas />} />
<Route path="snippets" element={<Snippets />} />
<Route path="snippets/snippet/:id" element={<Snippets />} />
<Route path="resources" element={<Resources />} />
<Route path="code-time" element={<CodeTime />} />
<Route path="pomodoro-timer" element={<PomodoroTimer />} />
<Route path="flashcards" element={<Flashcards />} />
<Route path="flashcards/:id" element={<CardSet />} />
<Route path="reference-books" element={<ReferenceBooks />} />
<Route path="notes" element={<Notes />} />
<Route path="notes/:workspace" element={<NotesCategory />} />
<Route path="notes/:workspace/:subject/*" element={<NotesSubject />} />
<Route path="university-analytics" element={<UniversityAnalytics />} />
<Route path="personalization" element={<Personalization />} />
<Route path="change-log" element={<Changelog />} />
</Route>
<Route path="auth" element={<Auth />} />
<Route path="*" element={<NotFound />} />
</Routes>
)
}
export default AppRouter

View File

@@ -1,23 +0,0 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function AuthHeader(): React.ReactElement {
return (
<>
<h1 className="mb-8 mt-32 flex items-center gap-2 whitespace-nowrap text-3xl font-semibold">
<Icon icon="tabler:hammer" className="text-5xl text-custom-500" />
<div>
LifeForge<span className="text-4xl text-custom-500"> .</span>
</div>
</h1>
<h2 className="text-center text-4xl font-semibold tracking-wide sm:text-5xl">
Welcome Back!
</h2>
<p className="mt-2 text-center text-base text-neutral-500 sm:mt-4 sm:text-xl">
Sign in to continue tracking your life.
</p>
</>
)
}
export default AuthHeader

View File

@@ -1,19 +0,0 @@
import React from 'react'
function AuthSideImage(): React.ReactElement {
return (
<section className="relative hidden h-full w-1/2 lg:flex">
<img src="/login.jpg" alt="Login" className="h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-br from-custom-500 to-custom-600 opacity-30" />
<div className="absolute inset-0 bg-neutral-900/50" />
<p className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 flex-col text-center text-5xl font-semibold tracking-wide text-neutral-100">
<span className="mb-2 text-2xl text-custom-400">
One day, You&apos;ll leave this world behind
</span>
So live a life you remember
</p>
</section>
)
}
export default AuthSideImage

View File

@@ -1,87 +0,0 @@
import React, { type FormEvent, useContext, useState } from 'react'
import { AuthContext } from '../../providers/AuthProvider'
import { toast } from 'react-toastify'
import { AUTH_ERROR_MESSAGES } from '../../constants/auth'
import { Icon } from '@iconify/react/dist/iconify.js'
function AuthSignInButton({
emailOrUsername,
password
}: {
emailOrUsername: string
password: string
}): React.ReactElement {
const [loading, setLoading] = useState(false)
const {
auth,
authenticate,
authWithOauth,
loginQuota: { quota, dismissQuota }
} = useContext(AuthContext)
function signIn(): void {
if (quota === 0) {
dismissQuota()
return
}
setLoading(true)
authenticate({ email: emailOrUsername, password })
.then(res => {
if (!res.startsWith('success')) {
toast.error(res)
if (res === AUTH_ERROR_MESSAGES.INVALID_CREDENTIALS) {
dismissQuota()
}
} else {
toast.success('Welcome back, ' + res.split(' ').slice(1).join(' '))
}
setLoading(false)
})
.catch(() => {
toast.error(AUTH_ERROR_MESSAGES.UNKNOWN_ERROR)
})
}
function signInWithGithub(): void {
setLoading(true)
authWithOauth('github')
.then(res => {
if (!res.startsWith('success')) {
toast.error(res)
} else {
toast.success('Welcome back, ' + res.split(' ').slice(1).join(' '))
}
setLoading(false)
})
.catch(() => {
toast.error(AUTH_ERROR_MESSAGES.UNKNOWN_ERROR)
})
}
return (
<div className="mt-6 flex flex-col gap-6">
<button
disabled={
emailOrUsername.length === 0 ||
password.length === 0 ||
loading ||
auth
}
onClick={signIn}
className="flex h-[4.6rem] items-center justify-center rounded-lg bg-custom-500 p-6 font-semibold uppercase tracking-widest text-neutral-100 transition-all hover:bg-custom-600 disabled:cursor-not-allowed disabled:bg-custom-700 disabled:text-neutral-200 dark:disabled:bg-custom-900 dark:disabled:text-neutral-400"
>
{loading ? <Icon icon="svg-spinners:180-ring" /> : 'Sign In'}
</button>
<button
type="button"
onClick={signInWithGithub}
className="flex items-center justify-center gap-3 rounded-lg bg-neutral-400 p-6 font-semibold uppercase tracking-widest text-neutral-100 transition-all hover:bg-neutral-500 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
<Icon icon="tabler:brand-github" className="text-2xl" />
Sign In with Github
</button>
</div>
)
}
export default AuthSignInButton

View File

@@ -1,53 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import React, { useState } from 'react'
import AuthSideImage from './components/AuthSideImage'
import AuthHeader from './components/AuthHeader'
import Input from '../components/general/Input'
import AuthSignInButton from './components/AuthSignInButtons'
function Auth(): React.JSX.Element {
const [emailOrUsername, setEmail] = useState('')
const [password, setPassword] = useState('')
function updateEmailOrUsername(
event: React.ChangeEvent<HTMLInputElement>
): void {
setEmail(event.target.value)
}
function updatePassword(event: React.ChangeEvent<HTMLInputElement>): void {
setPassword(event.target.value)
}
return (
<>
<section className="flex h-full w-full flex-col items-center overflow-y-auto px-12 pb-12 lg:w-1/2">
<AuthHeader />
<div className="mt-12 flex w-full max-w-md flex-col gap-8">
<Input
name="Email or Username"
placeholder="someone@somemail.com"
icon="tabler:user"
value={emailOrUsername}
updateValue={updateEmailOrUsername}
/>
<Input
name="Password"
placeholder="••••••••••••••••"
icon="tabler:key"
isPassword
value={password}
updateValue={updatePassword}
/>
<AuthSignInButton
emailOrUsername={emailOrUsername}
password={password}
/>
</div>
</section>
<AuthSideImage />
</>
)
}
export default Auth

View File

@@ -1,101 +0,0 @@
import React, { Fragment, useContext } from 'react'
import { Icon } from '@iconify/react'
import { GlobalStateContext } from '../providers/GlobalStateProvider'
import { AuthContext } from '../providers/AuthProvider'
import { Menu, Transition } from '@headlessui/react'
import { toast } from 'react-toastify'
import MenuItem from './general/HamburgerMenu/MenuItem'
export default function Header(): React.JSX.Element {
const { sidebarExpanded, toggleSidebar } = useContext(GlobalStateContext)
const { userData, getAvatarURL, logout } = useContext(AuthContext)
return (
<header className="relative z-[9990] flex flex-col p-8 sm:p-12">
<div className="flex w-full items-center justify-between gap-8">
<div className="flex w-full items-center gap-4">
{!sidebarExpanded && (
<button
onClick={toggleSidebar}
className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
>
<Icon icon="tabler:menu" className="text-2xl" />
</button>
)}
<search className="hidden w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50 lg:flex">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
placeholder="Quick navigate & search ... (Press / to focus)"
className="w-full bg-transparent text-neutral-800 placeholder:text-neutral-400 focus:outline-none"
/>
</search>
</div>
<div className="flex items-center gap-4">
<button className="relative rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:hover:bg-neutral-800">
<Icon icon="tabler:bell" className="text-2xl" />
<div className="absolute bottom-4 right-4 h-2 w-2 rounded-full bg-red-500" />
</button>
<Menu as="div" className="relative text-left">
<Menu.Button className="flex items-center gap-3">
<div className="h-9 w-9 overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
<img
src={getAvatarURL()}
alt="avatar"
className="h-full w-full object-cover"
/>
</div>
<div className="hidden flex-col items-start md:flex">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
{userData?.name}
</div>
<div className="text-sm text-neutral-400">
{userData?.email}
</div>
</div>
<Icon
icon="tabler:chevron-down"
className="stroke-[2px] text-neutral-500"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-4 w-56 overflow-hidden rounded-lg bg-neutral-100 shadow-lg focus:outline-none dark:bg-neutral-800">
<div className="py-1">
<MenuItem
onClick={() => {}}
icon="tabler:user-cog"
text="Account settings"
/>
<MenuItem
onClick={() => {
logout()
toast.warning('Logged out successfully!')
}}
icon="tabler:logout"
text="Sign out"
/>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
<search className="mt-4 flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50 lg:hidden">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
placeholder="Quick navigate & search ... (Press / to focus)"
className="w-full bg-transparent text-neutral-800 placeholder:text-neutral-400 focus:outline-none"
/>
</search>
</header>
)
}

View File

@@ -1,9 +0,0 @@
import React from 'react'
function SidebarDivider(): React.JSX.Element {
return (
<li className="my-4 h-px shrink-0 bg-neutral-200 dark:bg-neutral-700" />
)
}
export default SidebarDivider

View File

@@ -1,34 +0,0 @@
import React, { useContext } from 'react'
import { GlobalStateContext } from '../../../providers/GlobalStateProvider'
import { Icon } from '@iconify/react'
function SidebarHeader(): React.JSX.Element {
const { sidebarExpanded, toggleSidebar } = useContext(GlobalStateContext)
return (
<div
className={`flex h-36 w-full items-center justify-between pl-6 transition-none ${
!sidebarExpanded ? 'my-8 overflow-hidden sm:my-2' : 'my-4'
}`}
>
<h1 className="ml-1 flex shrink-0 items-center gap-2 whitespace-nowrap text-xl font-semibold text-neutral-800 dark:text-neutral-100">
<Icon icon="tabler:hammer" className="text-3xl text-custom-500" />
{sidebarExpanded && (
<div>
LifeForge<span className="text-3xl text-custom-500"> .</span>
</div>
)}
</h1>
{sidebarExpanded && (
<button
onClick={toggleSidebar}
className="p-6 text-neutral-500 transition-all hover:text-neutral-800 dark:hover:text-neutral-100"
>
<Icon icon="tabler:menu" className="text-2xl" />
</button>
)}
</div>
)
}
export default SidebarHeader

View File

@@ -1,132 +0,0 @@
import { Icon } from '@iconify/react'
import React, { useContext, useEffect, useState } from 'react'
import { GlobalStateContext } from '../../../providers/GlobalStateProvider'
import { useLocation } from 'react-router'
import { Link } from 'react-router-dom'
interface SidebarItemProps {
icon: string
name: string
subsection?: string[][]
isMainSidebarItem?: boolean
active?: boolean
}
function titleToPath(title: string): string {
return title.toLowerCase().replace(' ', '-')
}
function SidebarItem({
icon,
name,
subsection,
isMainSidebarItem,
active
}: SidebarItemProps): React.JSX.Element {
const { sidebarExpanded, toggleSidebar } =
isMainSidebarItem === true
? useContext(GlobalStateContext)
: { sidebarExpanded: true }
const [subsectionExpanded, setSubsectionExpanded] = useState(false)
function toggleSubsection(): void {
setSubsectionExpanded(!subsectionExpanded)
}
const location = useLocation()
useEffect(() => {
setSubsectionExpanded(
location.pathname.slice(1).startsWith(titleToPath(name))
)
}, [location.pathname])
return (
<>
<li
className={`relative flex items-center gap-6 px-4 font-medium transition-all ${
location.pathname.slice(1).startsWith(titleToPath(name))
? "text-neutral-800 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-1 after:-translate-y-1/2 after:rounded-full after:bg-custom-500 after:content-[''] dark:text-neutral-100"
: 'text-neutral-500 dark:text-neutral-400'
}`}
>
<div
className={`relative flex w-full items-center justify-between gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-200/30 dark:hover:bg-neutral-800 ${
location.pathname.slice(1).startsWith(titleToPath(name)) ||
active === true
? 'bg-neutral-200/50 dark:bg-neutral-800'
: ''
}`}
>
<div className="flex min-w-0 items-center gap-6">
<Icon
icon={icon}
className={`h-6 w-6 shrink-0 ${
location.pathname.slice(1).startsWith(titleToPath(name)) ||
active === true
? 'text-custom-500'
: ''
}`}
/>
<span className="w-full truncate">{sidebarExpanded && name}</span>
</div>
<Link
onClick={() => {
if (window.innerWidth < 1024) {
toggleSidebar()
}
}}
to={`./${titleToPath(name)}`}
className="absolute left-0 top-0 h-full w-full rounded-lg"
/>
{sidebarExpanded && (
<div className="relative flex items-center justify-between">
{subsection !== undefined && (
<button
onClick={toggleSubsection}
className="rounded-full p-1 hover:bg-neutral-200 dark:hover:bg-neutral-700/50"
>
<Icon
icon="tabler:chevron-right"
className={`stroke-[2px] text-neutral-400 transition-all ${
subsectionExpanded ? 'rotate-90' : ''
}`}
/>
</button>
)}
</div>
)}
</div>
</li>
{sidebarExpanded && subsection !== undefined && (
<li
className={`flex h-auto shrink-0 flex-col gap-2 overflow-hidden ${
subsectionExpanded ? 'max-h-[1000px] py-2' : 'max-h-0 py-0'
}`}
>
{subsection.map(
([subsectionName, subsectionIcon, subsectionLink]) => (
<Link
to={`./${titleToPath(name)}/${subsectionLink}`}
key={subsectionName}
className={`mx-4 flex items-center gap-4 rounded-lg py-4 pl-[3.8rem] font-medium transition-all hover:bg-neutral-200/30 dark:hover:bg-neutral-800 ${
location.pathname.split('/').slice(1)[0] ===
titleToPath(name) &&
location.pathname.split('/').slice(1)[1] === subsectionLink
? 'bg-neutral-200/50 dark:bg-neutral-800'
: 'text-neutral-400'
}`}
>
<Icon icon={subsectionIcon} className="h-6 w-6" />
{subsectionName}
</Link>
)
)}
</li>
)}
</>
)
}
export default SidebarItem

View File

@@ -1,94 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
import React, { useContext, useEffect, useState } from 'react'
import { GlobalStateContext } from '../../../providers/GlobalStateProvider'
import SidebarItem from './SidebarItem'
import SidebarTitle from './SidebarTitle'
import SidebarDivider from './SidebarDivider'
import SIDEBAR_ITEMS from '../../../constants/sidebar'
import { toast } from 'react-toastify'
import { type INotesWorkspace } from '../../../modules/Notes'
function SidebarItems(): React.JSX.Element {
const { sidebarExpanded } = useContext(GlobalStateContext)
const [sidebarItems, setSidebarItems] = useState(SIDEBAR_ITEMS)
const [notesCategories, setNotesCategories] = useState<
INotesWorkspace[] | 'error' | 'loading'
>('loading')
function updateNotesCategory(): void {
setNotesCategories('loading')
fetch(`${import.meta.env.VITE_API_HOST}/notes/workspace/list`)
.then(async response => {
const data = await response.json()
setNotesCategories(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setNotesCategories('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateNotesCategory()
}, [])
useEffect(() => {
if (notesCategories !== 'loading' && notesCategories !== 'error') {
setSidebarItems(
sidebarItems.map(item => {
if (item.name === 'Notes') {
return {
...item,
subsection: notesCategories.map(({ name, icon, id }) => [
name,
icon,
id
])
}
} else {
return item
}
})
)
}
}, [notesCategories])
return (
<ul className="flex flex-col gap-1 overflow-y-hidden overscroll-none pb-6 hover:overflow-y-scroll">
{sidebarItems.map(({ type, name, icon, subsection }, index) => {
switch (type) {
case 'item':
return (
<SidebarItem
key={type + index}
name={name!}
icon={icon ?? ''}
subsection={subsection}
isMainSidebarItem
/>
)
case 'title':
return sidebarExpanded ? (
<SidebarTitle key={type + index} name={name!} />
) : (
<></>
)
case 'divider':
return <SidebarDivider key={type + index} />
default:
return <></>
}
})}
</ul>
)
}
export default SidebarItems

View File

@@ -1,32 +0,0 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
interface SidebarItemProps {
name: string
actionButtonIcon?: string
actionButtonOnClick?: () => void
}
function SidebarTitle({
name,
actionButtonIcon,
actionButtonOnClick
}: SidebarItemProps): React.JSX.Element {
return (
<li className="flex items-center justify-between gap-4 py-4 pl-8 pr-5 pt-2 transition-all">
<h3 className="text-sm font-semibold uppercase tracking-widest text-neutral-600">
{name}
</h3>
{actionButtonIcon !== undefined && (
<button
onClick={actionButtonOnClick}
className="flex items-center rounded-md p-2 text-neutral-600 hover:bg-neutral-200/50 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
>
<Icon icon={actionButtonIcon} className="h-5 w-5" />
</button>
)}
</li>
)
}
export default SidebarTitle

View File

@@ -1,22 +0,0 @@
/* eslint-disable multiline-ternary */
import React, { useContext } from 'react'
import { GlobalStateContext } from '../../providers/GlobalStateProvider'
import SidebarItems from './components/SidebarItems'
import SidebarHeader from './components/SidebarHeader'
export default function Sidebar(): React.JSX.Element {
const { sidebarExpanded } = useContext(GlobalStateContext)
return (
<aside
className={`${
sidebarExpanded
? 'w-full sm:w-1/2 lg:w-3/12 xl:w-1/5'
: 'w-0 sm:w-[5.4rem]'
} absolute left-0 top-0 z-[9999] flex h-full shrink-0 flex-col rounded-r-2xl bg-neutral-50 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] duration-300 dark:bg-[#1e1e1e] lg:relative`}
>
<SidebarHeader />
<SidebarItems />
</aside>
)
}

View File

@@ -1,87 +0,0 @@
import React, { useEffect, useState } from 'react'
import Modal from './Modal'
import { Colorful, EditableInput } from '@uiw/react-color'
import { Icon } from '@iconify/react/dist/iconify.js'
function checkContrast(hexcolor: string): string {
const r = parseInt(hexcolor.substr(1, 2), 16)
const g = parseInt(hexcolor.substr(3, 2), 16)
const b = parseInt(hexcolor.substr(5, 2), 16)
const yiq = (r * 299 + g * 587 + b * 114) / 1000
return yiq >= 128 ? '#000000' : '#ffffff'
}
function ColorPickerModal({
isOpen,
setOpen,
color,
setColor
}: {
isOpen: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
color: string
setColor: React.Dispatch<React.SetStateAction<string>>
}): React.ReactElement {
const [innerColor, setInnerColor] = useState(color.toLowerCase())
function confirmColor(): void {
setColor(innerColor)
setOpen(false)
}
useEffect(() => {
setInnerColor(color.toLowerCase())
}, [color])
return (
<Modal isOpen={isOpen}>
<div className="mb-8 flex items-center justify-between ">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon icon="tabler:palette" className="h-7 w-7" />
Color picker
</h1>
<button
onClick={() => {
setOpen(false)
}}
className="rounded-md p-2 text-neutral-100 transition-all hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
<Colorful
color={innerColor}
onChange={color => {
setInnerColor(color.hex)
}}
disableAlpha
className="!w-full"
/>
<style
dangerouslySetInnerHTML={{
__html: `.w-color-editable-input input {
background-color: ${innerColor} !important;
color: ${checkContrast(innerColor)} !important;
}`
}}
/>
<EditableInput
label="Hex"
value={innerColor}
onChange={e => {
setInnerColor(`#${e.target.value}`)
}}
className="mt-4 border-0 p-4 text-2xl font-semibold"
/>
<button
onClick={confirmColor}
className="flex items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-800 transition-all hover:bg-custom-600"
>
<Icon icon="tabler:check" className="h-6 w-6" />
SELECT
</button>
</Modal>
)
}
export default ColorPickerModal

View File

@@ -1,38 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function EmptyStateScreen({
setModifyModalOpenType,
title,
description,
icon,
ctaContent
}: {
setModifyModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
title: string
description: string
icon: string
ctaContent: string
}): React.ReactElement {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6 ">
<Icon icon={icon} className="h-32 w-32" />
<h2 className="text-3xl font-semibold">{title}</h2>
<p className="-mt-2 text-xl text-neutral-500">{description}</p>
<button
onClick={() => {
setModifyModalOpenType('create')
}}
className="mt-6 flex items-center gap-2 rounded-full bg-custom-500 p-4 px-6 pr-7 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-custom-600 dark:text-neutral-800"
>
<Icon icon="tabler:plus" className="text-xl" />
{ctaContent}
</button>
</div>
)
}
export default EmptyStateScreen

View File

@@ -1,13 +0,0 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function Error({ message }: { message: string }): React.ReactElement {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<Icon icon="tabler:alert-triangle" className="h-12 w-12 text-red-500" />
<p className="text-lg font-medium text-red-500">{message}</p>
</div>
)
}
export default Error

View File

@@ -1,22 +0,0 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function GoBackButton({
onClick
}: {
onClick: () => void
}): React.ReactElement {
return (
<button
onClick={onClick}
className="-ml-1 mb-2 flex w-min items-center gap-1 rounded-lg pl-0 text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-100 sm:gap-2"
>
<Icon icon="tabler:chevron-left" className="text-lg sm:text-xl" />
<span className="whitespace-nowrap text-base font-medium sm:text-lg">
Go back
</span>
</button>
)
}
export default GoBackButton

View File

@@ -1,39 +0,0 @@
import { Menu } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function MenuItem({
icon,
text,
isRed = false,
onClick
}: {
icon: string
text: string
isRed?: boolean
onClick: () => void
}): React.ReactElement {
return (
<Menu.Item>
{({ active }) => (
<button
onClick={onClick}
className={`${
active
? `bg-neutral-200/50 ${
isRed ? 'text-red-600' : 'text-neutral-800'
} dark:bg-neutral-700 dark:text-neutral-100`
: isRed
? 'text-red-500'
: 'text-neutral-500'
} flex w-full items-center p-4`}
>
<Icon icon={icon} className="h-5 w-5" />
<span className="ml-2">{text}</span>
</button>
)}
</Menu.Item>
)
}
export default MenuItem

View File

@@ -1,34 +0,0 @@
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function HamburgerMenu({
children,
position
}: {
children: React.ReactNode
position: string
}): React.ReactElement {
return (
<Menu as="div" className={position}>
<Menu.Button className="rounded-md p-2 text-neutral-500 hover:bg-neutral-200/50 hover:text-neutral-500 dark:hover:bg-neutral-700/30">
<Icon icon="tabler:dots-vertical" className="h-5 w-5" />
</Menu.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
className="absolute right-0 top-3 z-50"
>
<Menu.Items className="mt-8 w-48 overflow-hidden overscroll-contain rounded-md bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
{children}
</Menu.Items>
</Transition>
</Menu>
)
}
export default HamburgerMenu

View File

@@ -1,74 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { Icon } from '@iconify/react/dist/iconify.js'
import React, { useState } from 'react'
import IconSelector from '.'
function IconInput({
name,
icon,
setIcon
}: {
name: string
icon: string
setIcon: React.Dispatch<React.SetStateAction<string>>
}): React.ReactElement {
const [iconSelectorOpen, setIconSelectorOpen] = useState(false)
function updateIcon(e: React.ChangeEvent<HTMLInputElement>): void {
setIcon(e.target.value)
}
return (
<>
<div className="group relative mt-6 flex items-center gap-1 rounded-t-lg border-b-2 border-neutral-500 bg-neutral-200/50 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] focus-within:border-custom-500 dark:bg-neutral-800/50">
<Icon
icon="tabler:icons"
className={`ml-6 h-6 w-6 shrink-0 ${
icon ? 'text-neutral-100' : 'text-neutral-500'
} group-focus-within:text-custom-500`}
/>
<div className="flex w-full items-center gap-2">
<span
className={`pointer-events-none absolute left-[4.2rem] font-medium tracking-wide text-neutral-500 group-focus-within:text-custom-500 ${
icon.length === 0
? 'top-1/2 -translate-y-1/2 group-focus-within:top-6 group-focus-within:text-[14px]'
: 'top-6 -translate-y-1/2 text-[14px]'
}`}
>
{name}
</span>
<div className="mr-12 mt-6 flex w-full items-center gap-2 pl-4">
<Icon
className={`h-4 w-4 shrink-0 rounded-full ${
!icon && 'hidden group-focus-within:block'
}`}
icon={icon || 'tabler:question-mark'}
/>
<input
value={icon}
onChange={updateIcon}
placeholder="tabler:cube"
className="h-8 w-full rounded-lg bg-transparent p-6 pl-0 tracking-wide placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-400"
/>
</div>
<button
onClick={() => {
setIconSelectorOpen(true)
}}
className="mr-4 shrink-0 rounded-lg p-2 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-900 focus:outline-none dark:hover:bg-neutral-500/30 dark:hover:text-neutral-200"
>
<Icon icon="tabler:chevron-down" className="h-6 w-6" />
</button>
</div>
</div>
<IconSelector
isOpen={iconSelectorOpen}
setOpen={setIconSelectorOpen}
setSelectedIcon={setIcon}
/>
</>
)
}
export default IconInput

View File

@@ -1,137 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable react/jsx-one-expression-per-line */
/* eslint-disable operator-linebreak */
/* eslint-disable implicit-arrow-linebreak */
/* eslint-disable react/prop-types */
import { Icon } from '@iconify/react'
import React, { useEffect, useState } from 'react'
import Input from './Input'
export interface IIconSetData {
title: string
total: number
prefix: string
uncategorized: string[]
categories: Record<string, string[]>
}
async function getIconSet(prefix: string): Promise<any> {
try {
const res: IIconSetData = await fetch(
`https://api.iconify.design/collection?prefix=${prefix}`
).then(async res => await res.json())
console.log(res)
if (!res.uncategorized) {
res.uncategorized = Object.values(res.categories).flat()
}
return res
} catch (err) {
console.error(err)
return null
}
}
function IconSet({
setOpen,
iconSet,
setSelectedIcon
}: {
setOpen: React.Dispatch<React.SetStateAction<boolean>>
iconSet: string
setSelectedIcon: React.Dispatch<React.SetStateAction<string>>
}): React.ReactElement {
const [searchTerm, setSearchTerm] = useState('')
const [currentTag, setCurrentTag] = useState<string | null>(null)
const [iconData, setIconData] = useState<IIconSetData | null>(null)
const [filteredIconList, setFilteredIconList] = useState<string[]>([])
useEffect(() => {
getIconSet(iconSet).then(data => {
setIconData(data)
setFilteredIconList(data.uncategorized)
})
}, [])
useEffect(() => {
if (iconData) {
if (currentTag) {
setFilteredIconList(
iconData.categories[currentTag].filter(icon =>
icon.toLowerCase().includes(searchTerm.toLowerCase())
)
)
} else {
setFilteredIconList(
iconData.uncategorized.filter(icon =>
icon.toLowerCase().includes(searchTerm.toLowerCase())
)
)
}
}
}, [searchTerm, currentTag, iconData])
return iconData ? (
<div className="flex min-h-0 w-full flex-col overflow-scroll p-8">
<h1 className="mb-6 flex flex-col items-center gap-1 text-center text-3xl font-semibold tracking-wide sm:inline">
{iconData.title}
</h1>
<Input
value={searchTerm}
setValue={setSearchTerm}
placeholder={`Search ${iconData.total.toLocaleString()} icons`}
icon="uil:search"
/>
{Object.keys(iconData.categories || {}).length > 0 && (
<div className="mt-4 flex flex-wrap justify-center gap-2">
{Object.keys(iconData.categories).map(
tag =>
tag && (
<button
key={tag}
type="button"
onClick={() => {
setCurrentTag(currentTag === tag ? null : tag)
}}
className={`${
currentTag === tag
? '!bg-neutral-200 !text-neutral-800 shadow-md'
: ''
} flex h-8 grow items-center justify-center whitespace-nowrap rounded-full bg-neutral-800 px-6 text-sm text-neutral-100 shadow-md transition-all md:grow-0`}
>
{tag}
</button>
)
)}
</div>
)}
<div className="mt-8 grid min-h-0 w-full grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3 pb-8">
{filteredIconList.map(icon => (
<button
key={icon}
type="button"
onClick={() => {
setSelectedIcon(`${iconSet}:${icon}`)
setOpen(false)
}}
className="flex cursor-pointer flex-col items-center rounded-lg p-4 transition-all hover:bg-neutral-800"
>
<Icon icon={`${iconSet}:${icon}`} width="32" height="32" />
<p className="-mb-0.5 mt-4 break-all text-center text-xs font-medium tracking-wide">
{icon.replace(/-/g, ' ')}
</p>
</button>
))}
</div>
</div>
) : (
<div className="flex w-full justify-center pb-8">
<Icon icon="svg-spinners:270-ring" className="h-8 w-8" />
</div>
)
}
export default IconSet

View File

@@ -1,179 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable react/jsx-one-expression-per-line */
/* eslint-disable operator-linebreak */
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-indent */
/* eslint-disable implicit-arrow-linebreak */
/* eslint-disable no-param-reassign */
import React, { useState } from 'react'
import { Icon } from '@iconify/react'
import { collections } from '@iconify/collections'
import Input from './Input'
const ICON_SETS = {
General: [],
'Animated Icons': [],
'Brands / Social': [],
Emoji: [],
'Maps / Flags': [],
Thematic: [],
'Archive / Unmaintained': [],
Other: []
}
Object.entries(collections).forEach(([key, value]) => {
value.prefix = key
ICON_SETS[value.category || 'Other'] = [
...ICON_SETS[value.category || 'Other'],
value
]
})
const ICON_COUNT = Object.values(ICON_SETS)
.flat()
.map(e => e.total)
.reduce((a, b) => a + b)
export default function IconSetList({
setCurrentIconSet
}: {
setCurrentIconSet: React.Dispatch<
React.SetStateAction<{
iconSet?: string
search?: string
}>
>
}): React.ReactElement {
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState(null)
const [iconFilterTerm, setIconFilterTerm] = useState('')
return (
<div className="overflow-scroll p-8 pb-2 pt-0">
<div className="flex w-full flex-col gap-2 sm:flex-row">
<Input
value={searchQuery}
setValue={setSearchQuery}
placeholder={`Search ${ICON_COUNT.toLocaleString()} icons`}
icon="uil:search"
/>
<button
type="button"
onClick={() => {
if (searchQuery) setCurrentIconSet({ search: searchQuery })
}}
className="flex items-center justify-center gap-1 rounded-md bg-neutral-200 px-6 py-4 font-medium text-neutral-900 shadow-md transition-all hover:bg-[#b3bdc9]"
>
Search
<Icon icon="uil:arrow-right" className="h-5 w-5 text-neutral-900" />
</button>
</div>
<div className="flex w-full flex-col items-center gap-8 lg:flex-row">
<div className="mt-4 flex w-full flex-wrap gap-2">
{Object.keys(ICON_SETS).map((category, index) => (
<button
key={category}
type="button"
onClick={() => {
setSelectedCategory(selectedCategory === index ? null : index)
}}
className={`${
selectedCategory === index
? 'bg-neutral-200 text-neutral-800 shadow-md'
: ''
} flex h-8 grow items-center justify-center whitespace-nowrap rounded-full bg-neutral-800 px-6 text-sm text-neutral-100 shadow-md transition-all md:grow-0`}
>
{category}
</button>
))}
</div>
<div className="w-full lg:w-3/5 xl:w-1/3">
<Input
value={iconFilterTerm}
setValue={setIconFilterTerm}
placeholder="Filter icon sets"
icon="uil:filter"
/>
</div>
</div>
<div className="mt-12 flex min-h-0 w-full flex-col items-center overflow-scroll">
<div className="flex w-full flex-col">
{Object.entries(ICON_SETS).map(
([category, iconSets], index) =>
Boolean(
(selectedCategory === null || selectedCategory === index) &&
iconSets.filter(
iconSet =>
!iconFilterTerm.trim() ||
iconSet.name
.toLowerCase()
.includes(iconFilterTerm.trim().toLowerCase())
).length
) && (
<div key={category} className="mb-6 w-full overflow-hidden">
<div className="relative mb-8 rounded-lg text-center text-2xl font-medium after:absolute after:-bottom-2 after:left-1/2 after:w-8 after:-translate-x-1/2 after:border-b-2 after:border-b-neutral-200">
{category}
</div>
<div className="icon-list grid grid-cols-[repeat(auto-fill,minmax(230px,1fr))] flex-wrap gap-4">
{iconSets.map(
iconSet =>
(!iconFilterTerm.trim() ||
iconSet.name
.toLowerCase()
.includes(iconFilterTerm.trim().toLowerCase())) && (
<button
key={iconSet.prefix}
type="button"
onClick={() => {
setCurrentIconSet({ iconSet: iconSet.prefix })
}}
className="sssm:flex-row flex w-full grow flex-col overflow-hidden rounded-md bg-neutral-800 shadow-lg"
>
<div className="sssm:w-36 text-neutral-00 flex w-full shrink-0 flex-col font-medium">
<div className="sssm:gap-3 flex h-full w-full items-center justify-center gap-5 px-4 py-6 ">
{iconSet.samples.map(sampleIcon => (
<Icon
key={sampleIcon}
icon={`${iconSet.prefix}:${sampleIcon}`}
className="h-8 w-8 shrink-0"
/>
))}
</div>
</div>
<div className="flex w-full flex-col justify-between px-4 pb-3 text-left">
<h3 className="truncate text-xl font-semibold">
{iconSet.name}
</h3>
<p className="mt-1 truncate text-sm">
By&nbsp;
<span className="underline">
{iconSet.author.name}
</span>
</p>
<div className="sssm:py-0 mt-4 flex w-full items-center justify-between border-t border-neutral-500 pt-4 text-sm">
<p>{iconSet.total} icons</p>
{iconSet.height && (
<div className="flex items-center">
<Icon
icon="icon-park-outline:auto-height-one"
width="20"
height="20"
/>
<p className="ml-1">{iconSet.height}</p>
</div>
)}
</div>
</div>
</button>
)
)}
</div>
</div>
)
)}
</div>
</div>
</div>
)
}

View File

@@ -1,33 +0,0 @@
/* eslint-disable object-curly-newline */
/* eslint-disable react/prop-types */
import { Icon } from '@iconify/react'
import React from 'react'
function Input({
value,
setValue,
placeholder,
icon
}: {
value: string
setValue: React.Dispatch<React.SetStateAction<string>>
placeholder: string
icon: string
}): React.ReactElement {
return (
<div className="flex w-full items-center gap-3 rounded-md bg-neutral-800 px-5 shadow-md">
<Icon icon={icon} className="h-5 w-5 text-neutral-200" />
<input
type="text"
className="w-full bg-transparent py-4 text-neutral-200 outline-none placeholder:text-neutral-100"
placeholder={placeholder}
value={value}
onChange={e => {
setValue(e.target.value)
}}
/>
</div>
)
}
export default Input

View File

@@ -1,151 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/require-array-sort-compare */
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable react/jsx-one-expression-per-line */
/* eslint-disable operator-linebreak */
/* eslint-disable implicit-arrow-linebreak */
/* eslint-disable react/prop-types */
import { Icon } from '@iconify/react'
import React, { useEffect, useState } from 'react'
import Input from './Input'
async function getIconSet(searchTerm: string): Promise<any> {
try {
const res = await fetch(
`https://cors-anywhere.thecodeblog.net/api.iconify.design/search?query=${searchTerm}&limit=9999`
)
const data = await res.json()
let iconList = []
if (data.icons.length) {
iconList = data.icons
} else {
iconList = []
}
const iconSets = data.collections
return {
iconList,
iconSets
}
} catch (err) {
console.error(err)
return null
}
}
function Search({
setOpen,
searchTerm,
setSelectedIcon,
setCurrentIconSet: setCurrentIconSetProp
}: {
setOpen: React.Dispatch<React.SetStateAction<boolean>>
searchTerm: string
setSelectedIcon: React.Dispatch<React.SetStateAction<string>>
setCurrentIconSet: React.Dispatch<
React.SetStateAction<{
iconSet?: string
search?: string
}>
>
}): React.ReactElement {
const [currentIconSet, setCurrentIconSet] = useState(null)
const [iconData, setIconData] = useState(null)
const [filteredIconList, setFilteredIconList] = useState([])
const [searchQuery, setSearchQuery] = useState(searchTerm || '')
useEffect(() => {
setIconData(null)
getIconSet(searchTerm)
.then(data => {
setIconData(data)
setFilteredIconList(data.iconList)
setCurrentIconSet(null)
})
.catch(err => {
console.error(err)
})
}, [searchTerm])
useEffect(() => {
if (iconData) {
setFilteredIconList(
currentIconSet
? iconData.iconList.filter(
e => e.split(':').shift() === currentIconSet
)
: iconData.iconList
)
}
}, [currentIconSet, iconData])
return iconData ? (
<div className="flex min-h-0 w-full flex-col overflow-scroll p-8">
<div className="flex w-full gap-2">
<Input
value={searchQuery}
setValue={setSearchQuery}
placeholder="Search icons"
icon="uil:search"
/>
<button
type="button"
onClick={() => {
if (searchQuery) setCurrentIconSetProp({ search: searchQuery })
}}
className="flex items-center justify-center gap-1 rounded-md bg-neutral-200 px-6 py-4 font-medium text-neutral-800 shadow-md transition-all hover:bg-[#b3bdc9]"
>
Search
<Icon icon="uil:arrow-right" className="h-5 w-5 text-neutral-800" />
</button>
</div>
{Object.keys(iconData.iconSets).length > 0 && (
<div className="mt-4 flex flex-wrap justify-center gap-2">
{Object.entries(iconData.iconSets)
.sort()
.map(([name, iconSet]) => (
<button
key={name}
type="button"
onClick={() => {
setCurrentIconSet(currentIconSet === name ? null : name)
}}
className={`${
currentIconSet === name
? 'bg-neutral-200 text-neutral-800 shadow-md'
: 'bg-neutral-800'
} flex h-8 grow items-center justify-center whitespace-nowrap rounded-full px-6 text-sm text-neutral-100 shadow-md transition-all md:grow-0`}
>
{iconSet.name}
</button>
))}
</div>
)}
<div className=" mt-8 grid min-h-0 grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3 pb-8">
{filteredIconList.map(icon => (
<button
key={icon}
type="button"
onClick={() => {
setSelectedIcon(icon.name || icon)
setOpen(false)
}}
className="flex cursor-pointer flex-col items-center rounded-lg p-4 transition-all hover:bg-neutral-800"
>
<Icon icon={icon.name || icon} width="32" height="32" />
<p className="-mb-0.5 mt-4 break-all text-center text-xs font-medium tracking-wide">
{(icon.name || icon).replace(/-/g, ' ')}
</p>
</button>
))}
</div>
</div>
) : (
<div className="flex w-full justify-center pb-8">
<Icon icon="svg-spinners:270-ring" className="h-8 w-8" />
</div>
)
}
export default Search

View File

@@ -1,103 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable no-nested-ternary */
/* eslint-disable react/jsx-one-expression-per-line */
/* eslint-disable react/jsx-indent */
/* eslint-disable operator-linebreak */
/* eslint-disable implicit-arrow-linebreak */
/* eslint-disable no-param-reassign */
/* eslint-disable react/prop-types */
import React, { useState } from 'react'
import { Icon } from '@iconify/react'
import IconSetList from './components/IconSetList'
import IconSet from './components/IconSet.tsx'
import Search from './components/Search'
function IconSelector({
isOpen,
setOpen,
setSelectedIcon
}: {
isOpen: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
setSelectedIcon: React.Dispatch<React.SetStateAction<string>>
}): React.ReactElement {
const [currentIconSet, setCurrentIconSet] = useState(null)
return (
<div
className={`absolute left-0 top-0 h-screen w-full bg-neutral-900 transition-colors duration-500 ${
isOpen ? 'z-[9999] bg-opacity-50' : 'z-[-1] bg-opacity-0 delay-500'
}`}
>
<div
className={`absolute left-0 top-0 flex h-screen w-full items-center justify-center transition-all duration-500 ${
isOpen ? 'translate-y-0' : 'translate-y-[110%]'
}`}
>
<div className="510:mx-16 relative mx-4 flex max-h-[calc(100vh-8rem)] w-full flex-col items-center justify-center rounded-lg bg-neutral-900 shadow-2xl lg:w-3/4">
<div className="mb-6 flex w-full items-center justify-between p-8 pb-0">
{currentIconSet ? (
<button
onClick={() => {
setCurrentIconSet(null)
}}
type="button"
className="flex items-center gap-2 text-lg"
>
<Icon icon="uil:arrow-left" className="h-7 w-7" />
Go Back
</button>
) : (
<div className="flex items-end gap-2">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon icon="tabler:icons" className="h-7 w-7" />
Icon Selector
</h1>
<p className="shrink-0 text-right text-sm sm:text-base">
powered by&nbsp;
<a
target="_blank"
href="https://iconify.thecodeblog.net"
className="underline"
rel="noreferrer"
>
Iconify
</a>
</p>
</div>
)}
<button
onClick={() => {
setOpen(false)
}}
className="rounded-md p-2 text-neutral-100 transition-all hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
{currentIconSet ? (
currentIconSet.search ? (
<Search
searchTerm={currentIconSet.search}
setCurrentIconSet={setCurrentIconSet}
setSelectedIcon={setSelectedIcon}
setOpen={setOpen}
/>
) : (
<IconSet
iconSet={currentIconSet.iconSet}
setSelectedIcon={setSelectedIcon}
setOpen={setOpen}
/>
)
) : (
<IconSetList setCurrentIconSet={setCurrentIconSet} />
)}
</div>
</div>
</div>
)
}
export default IconSelector

View File

@@ -1,58 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function Input({
name,
placeholder,
icon,
value,
updateValue,
isPassword = false,
darker = false,
additionalClassName = ''
}: {
name: string
placeholder: string
icon: string
value: string
updateValue: (e: React.ChangeEvent<HTMLInputElement>) => void
isPassword?: boolean
darker?: boolean
additionalClassName?: string
}): React.ReactElement {
return (
<div
className={`group relative flex items-center gap-1 rounded-t-lg border-b-2 border-neutral-500 bg-neutral-200/50 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] focus-within:border-custom-500 ${
darker ? 'dark:bg-neutral-800/50' : 'dark:bg-neutral-800'
} ${additionalClassName}`}
>
<Icon
icon={icon}
className={`ml-6 h-6 w-6 shrink-0 ${
value ? 'text-neutral-100' : 'text-neutral-500'
} group-focus-within:text-custom-500`}
/>
<div className="flex w-full items-center gap-2">
<span
className={`pointer-events-none absolute left-[4.2rem] font-medium tracking-wide text-neutral-500 group-focus-within:text-custom-500 ${
value.length === 0
? 'top-1/2 -translate-y-1/2 group-focus-within:top-6 group-focus-within:text-[14px]'
: 'top-6 -translate-y-1/2 text-[14px]'
}`}
>
{name}
</span>
<input
type={isPassword ? 'password' : 'text'}
value={value}
onChange={updateValue}
placeholder={placeholder}
className="mt-6 h-8 w-full rounded-lg bg-transparent p-6 pl-4 tracking-wider placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-500"
/>
</div>
</div>
)
}
export default Input

View File

@@ -1,10 +0,0 @@
import React from 'react'
export default function Loading(): React.ReactElement {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<span className="loader"></span>
<p className="text-lg font-medium text-neutral-500">Loading data</p>
</div>
)
}

View File

@@ -1,29 +0,0 @@
import React from 'react'
function Modal({
isOpen,
children
}: {
isOpen: boolean
children: React.ReactNode
}): React.ReactElement {
return (
<div
className={`fixed left-0 top-0 h-screen w-full bg-neutral-950/60 backdrop-blur-md transition-opacity ease-linear ${
isOpen
? 'z-[9999] opacity-100'
: 'z-[-1] opacity-0 [transition:z-index_0.1s_linear_0.5s,opacity_0.1s_linear_0.1s]'
}`}
>
<div
className={`absolute ${
isOpen ? 'top-1/2' : 'top-[200vh]'
} left-1/2 flex max-h-[calc(100vh-8rem)] max-w-[calc(100vw-4rem)] -translate-x-1/2 -translate-y-1/2 flex-col rounded-xl bg-neutral-100 p-6 transition-all duration-300 dark:bg-neutral-900`}
>
{children}
</div>
</div>
)
}
export default Modal

View File

@@ -1,63 +0,0 @@
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react'
import React from 'react'
interface ModuleHeaderPropsWithHamburgerMenu {
title: string | React.ReactNode
desc?: string | React.ReactNode
hasHamburgerMenu?: false
hamburgerMenuItems?: never
}
interface ModuleHeaderPropsWithHamburgerMenuItems {
title: string | React.ReactNode
desc?: string | React.ReactNode
hasHamburgerMenu?: true
hamburgerMenuItems?: React.ReactNode
}
type ModuleHeaderProps =
| ModuleHeaderPropsWithHamburgerMenu
| ModuleHeaderPropsWithHamburgerMenuItems
function ModuleHeader({
title,
desc,
hasHamburgerMenu = false,
hamburgerMenuItems
}: ModuleHeaderProps): React.JSX.Element {
return (
<div className="flex items-center justify-between gap-8">
<div className="flex items-center gap-4">
<div className="flex flex-col gap-1">
<h1 className="flex items-center gap-3 text-3xl font-semibold text-neutral-800 dark:text-neutral-100 md:text-4xl">
{title}
</h1>
{desc !== undefined && <div className="text-neutral-500">{desc}</div>}
</div>
</div>
{hasHamburgerMenu && (
<Menu as="div" className="relative overscroll-contain">
<Menu.Button className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:hover:bg-neutral-800/50 dark:hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="h-5 w-5" />
</Menu.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
className="absolute right-0 top-3"
>
<Menu.Items className="mt-12 w-48 overflow-hidden overscroll-contain rounded-md bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
{hamburgerMenuItems}
</Menu.Items>
</Transition>
</Menu>
)}
</div>
)
}
export default ModuleHeader

View File

@@ -1,33 +0,0 @@
import { Icon } from '@iconify/react'
import React from 'react'
import { Link } from 'react-router-dom'
function NotFound(): React.JSX.Element {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<h1 className="text-[10rem] text-custom-500">;-;</h1>
<h1 className="text-4xl font-semibold">Page not found</h1>
<h2 className="-mt-2 text-xl text-neutral-500">
The page you are looking for does not exist.
</h2>
<div className="mt-6 flex items-center gap-4">
<Link
to="/"
className="flex items-center rounded-lg bg-custom-500 px-6 py-4 font-medium uppercase tracking-widest text-white shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-custom-600"
>
<Icon icon="tabler:arrow-left" className="mr-2 h-5 w-5" />
Return home
</Link>
<Link
to="bug-report"
className="flex items-center rounded-lg bg-neutral-200 px-6 py-4 font-medium uppercase tracking-widest shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-300 dark:bg-neutral-800"
>
<Icon icon="tabler:bug" className="mr-2 h-5 w-5" />
Report a bug
</Link>
</div>
</div>
)
}
export default NotFound

View File

@@ -1,6 +0,0 @@
export const AUTH_ERROR_MESSAGES = {
INVALID_CREDENTIALS: 'Invalid email or password.',
UNKNOWN_ERROR: 'Unknown error. Please file a bug report.',
DATABASE_NOT_READY: 'Database not ready. Please try again later.',
QUOTA_EXCEEDED: 'Quota exceeded. Please try again later.'
}

View File

@@ -1,170 +0,0 @@
const FILE_ICONS = {
pptx: 'tabler:presentation-analytics',
ppt: 'tabler:presentation-analytics',
docx: 'tabler:file-text',
doc: 'tabler:file-text',
xlsx: 'tabler:file-spreadsheet',
xls: 'tabler:file-spreadsheet',
pdf: 'tabler:file-type-pdf',
txt: 'tabler:file-type-txt',
zip: 'tabler:file-zip',
tex: 'tabler:code',
jpg: 'tabler:photo',
png: 'tabler:photo',
jpeg: 'tabler:photo',
gif: 'tabler:photo',
svg: 'tabler:svg',
mp4: 'tabler:movie',
mov: 'tabler:movie',
avi: 'tabler:movie',
mkv: 'tabler:movie',
mp3: 'tabler:music',
wav: 'tabler:music',
aac: 'tabler:music',
flac: 'tabler:music',
wma: 'tabler:music',
ogg: 'tabler:music',
m4a: 'tabler:music',
'3gp': 'tabler:music',
'3g2': 'tabler:music',
'3ga': 'tabler:music',
'7z': 'tabler:file-zip',
aif: 'tabler:music',
aiff: 'tabler:music',
amr: 'tabler:music',
asf: 'tabler:movie',
au: 'tabler:music',
bin: 'tabler:binary',
dmg: 'tabler:binary',
dmgpart: 'tabler:binary',
dmgvolume: 'tabler:binary',
dll: 'tabler:binary',
exe: 'tabler:binary',
img: 'tabler:binary',
iso: 'tabler:binary',
mdf: 'tabler:binary',
nrg: 'tabler:binary',
o: 'tabler:binary',
pkg: 'tabler:binary',
qcow: 'tabler:binary',
qcow2: 'tabler:binary',
qcow2c: 'tabler:binary',
r: 'tabler:binary',
rpm: 'tabler:binary',
sparsebundle: 'tabler:binary',
sparseimage: 'tabler:binary',
toast: 'tabler:binary',
vcd: 'tabler:binary',
vmdk: 'tabler:binary',
vmem: 'tabler:binary',
vmx: 'tabler:binary',
vmxf: 'tabler:binary',
xar: 'tabler:binary',
zipx: 'tabler:binary',
a: 'tabler:binary',
ar: 'tabler:binary',
deb: 'tabler:binary',
lbr: 'tabler:binary',
lha: 'tabler:binary',
mar: 'tabler:binary',
rsc: 'tabler:binary',
run: 'tabler:binary',
shar: 'tabler:binary',
uue: 'tabler:binary',
xip: 'tabler:binary',
z: 'tabler:binary',
apk: 'tabler:brand-android',
app: 'tabler:brand-apple',
bat: 'tabler:file-code',
cmd: 'tabler:file-code',
com: 'tabler:file-code',
cpl: 'tabler:file-code',
csh: 'tabler:file-code',
gadget: 'tabler:file-code',
inf1: 'tabler:file-code',
ins: 'tabler:file-code',
inx: 'tabler:file-code',
isu: 'tabler:file-code',
job: 'tabler:file-code',
jse: 'tabler:file-code',
ksh: 'tabler:file-code',
lnk: 'tabler:file-code',
ms: 'tabler:file-code',
msc: 'tabler:file-code',
msh: 'tabler:file-code',
paf: 'tabler:file-code',
pif: 'tabler:file-code',
ps1: 'tabler:file-code',
reg: 'tabler:file-code',
rgs: 'tabler:file-code',
sct: 'tabler:file-code',
shb: 'tabler:file-code',
shs: 'tabler:file-code',
u3p: 'tabler:file-code',
vb: 'tabler:file-code',
vbe: 'tabler:file-code',
vbs: 'tabler:file-code',
vbscript: 'tabler:file-code',
workflow: 'tabler:file-code',
ws: 'tabler:file-code',
wsf: 'tabler:file-code',
xpl: 'tabler:file-code',
action: 'tabler:file-code',
cgi: 'tabler:file-code',
jar: 'tabler:file-code',
pl: 'tabler:file-code',
plx: 'tabler:file-code',
py: 'tabler:file-code',
rb: 'tabler:file-code',
scr: 'tabler:file-code',
c: 'tabler:file-code',
cc: 'tabler:file-code',
class: 'tabler:file-code',
cpp: 'tabler:file-code',
cs: 'tabler:file-code',
cxx: 'tabler:file-code',
dylib: 'tabler:file-code',
lib: 'tabler:file-code',
swift: 'tabler:file-code',
h: 'tabler:file-code',
hh: 'tabler:file-code',
hpp: 'tabler:file-code',
hxx: 'tabler:file-code',
php: 'tabler:file-code',
pm: 'tabler:file-code',
sh: 'tabler:file-code',
vcxproj: 'tabler:file-code',
xcodeproj: 'tabler:file-code',
xib: 'tabler:file-code',
css: 'tabler:file-code',
less: 'tabler:file-code',
sass: 'tabler:file-code',
scss: 'tabler:file-code',
styl: 'tabler:file-code',
html: 'tabler:file-code',
htm: 'tabler:file-code',
js: 'tabler:file-code',
jsx: 'tabler:file-code',
mjs: 'tabler:file-code',
ts: 'tabler:file-code',
tsx: 'tabler:file-code',
vue: 'tabler:file-code',
yml: 'tabler:file-code',
yaml: 'tabler:file-code',
json: 'tabler:file-code',
lock: 'tabler:file-code',
md: 'tabler:file-code',
markdown: 'tabler:file-code',
rtf: 'tabler:file-code',
csv: 'tabler:file-code',
dat: 'tabler:file-code',
db: 'tabler:file-code',
dbf: 'tabler:file-code',
log: 'tabler:file-code',
mdb: 'tabler:file-code',
sav: 'tabler:file-code',
sql: 'tabler:file-code',
tar: 'tabler:file-code'
}
export default FILE_ICONS

View File

@@ -1,88 +0,0 @@
interface ISidebarItem {
type: 'item' | 'title' | 'divider'
name?: string
icon?: string
subsection?: string[][]
}
const SIDEBAR_ITEMS: ISidebarItem[] = [
{ type: 'item', name: 'Dashboard', icon: 'tabler:layout-dashboard' },
{ type: 'divider' },
{ type: 'title', name: 'Productivity' },
{ type: 'item', name: 'Todo List', icon: 'tabler:list-check' },
{ type: 'item', name: 'Daily Schedule', icon: 'tabler:clipboard-list' },
{ type: 'item', name: 'Calendar', icon: 'tabler:calendar' },
{ type: 'divider' },
{ type: 'title', name: 'Development' },
{
type: 'item',
name: 'Projects',
icon: 'tabler:clipboard'
},
{ type: 'item', name: 'Idea Box', icon: 'tabler:bulb' },
{ type: 'item', name: 'Snippets', icon: 'tabler:code-dots' },
{ type: 'item', name: 'Resources', icon: 'tabler:book' },
{ type: 'item', name: 'Code Time', icon: 'tabler:code' },
{ type: 'divider' },
{ type: 'title', name: 'Study' },
{ type: 'item', name: 'Pomodoro Timer', icon: 'tabler:clock-bolt' },
{ type: 'item', name: 'Flashcards', icon: 'tabler:cards' },
{
type: 'item',
name: 'Notes',
icon: 'tabler:notebook'
},
{
type: 'item',
name: 'Reference Books',
icon: 'tabler:books',
subsection: [
['Mathematics', 'tabler:calculator'],
['Physics', 'tabler:atom']
]
},
{ type: 'divider' },
{ type: 'title', name: 'Lifestyle' },
{ type: 'item', name: 'Blog', icon: 'tabler:file-text' },
{
type: 'item',
name: 'Travel',
icon: 'tabler:plane',
subsection: [
['Places', 'tabler:map-2'],
['Trips', 'tabler:map-pin'],
['Photos', 'tabler:photo']
]
},
{ type: 'item', name: 'Achievements', icon: 'tabler:award' },
{ type: 'item', name: 'Memory Archive', icon: 'tabler:archive' },
{ type: 'divider' },
{ type: 'title', name: 'Finance' },
{
type: 'item',
name: 'Wallet',
icon: 'tabler:currency-dollar',
subsection: [
['Balance', 'tabler:wallet'],
['Transactions', 'tabler:arrows-exchange'],
['Budgets', 'tabler:coin'],
['Reports', 'tabler:chart-bar']
]
},
{ type: 'item', name: 'Wish List', icon: 'tabler:heart' },
{ type: 'divider' },
{ type: 'title', name: 'Confidential' },
{ type: 'item', name: 'Contacts', icon: 'tabler:users' },
{ type: 'item', name: 'Passwords', icon: 'tabler:key' },
{ type: 'divider' },
{ type: 'title', name: 'Settings' },
{ type: 'item', name: 'Settings', icon: 'tabler:settings' },
{ type: 'item', name: 'Plugins', icon: 'tabler:plug' },
{ type: 'item', name: 'Personalization', icon: 'tabler:palette' },
{ type: 'item', name: 'Server Status', icon: 'tabler:server' },
{ type: 'divider' },
{ type: 'item', name: 'About', icon: 'tabler:info-circle' },
{ type: 'item', name: 'Change Log', icon: 'tabler:file-text' }
]
export default SIDEBAR_ITEMS

View File

@@ -1,40 +0,0 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import type Client from 'pocketbase'
import { useEffect, useRef, useState } from 'react'
import Pocketbase from 'pocketbase'
export default function usePocketbase(): {
pocketbase: any
data: any
error: any
loading: boolean
} {
const [state, setState] = useState<{
data: any
error: any
loading: boolean
}>({
data: null,
error: null,
loading: true
})
const pocketbase = useRef<Client>()
useEffect(() => {
;(async () => {
setState(s => ({ ...s, loading: true }))
try {
pocketbase.current = new Pocketbase(
import.meta.env.VITE_POCKETBASE_ENDPOINT as string
)
await pocketbase.current.collection('users').getList(1, 1)
setState(s => ({ ...s, loading: false }))
} catch (error) {
setState(s => ({ ...s, error, loading: false }))
}
})()
}, [])
return { pocketbase: pocketbase.current, ...state }
}

View File

@@ -1,386 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Urbanist:wght@100;200;300;400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
font-family: 'Urbanist', sans-serif;
scrollbar-color: rgba(64, 64, 64, 0.5) rgba(38, 38, 38, 0.5);
scrollbar-width: thin;
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
}
*:focus {
outline: none;
}
body *:not(input) {
@apply transition-all;
}
.stroke-\[2px\] > path {
stroke-width: 2.5px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(38, 38, 38, 0.5);
}
::-webkit-scrollbar-thumb:vertical {
background: rgba(64, 64, 64, 0.5);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(88, 88, 88, 0.5);
}
input:autofill {
background: rgb(38 38 38); /* or any other */
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px rgb(38, 38, 38) inset !important;
-webkit-text-fill-color: rgba(250, 250, 250, 1) !important;
}
.loader {
width: 36px;
height: 36px;
border: 4px solid rgb(115 115 115);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.small-loader-light {
width: 20px;
height: 20px;
border: 2px solid rgb(245 245 245);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.small-loader-dark {
width: 20px;
height: 20px;
border: 2px solid rgb(38 38 38);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.w-color-editable-input {
@apply !px-0;
}
.w-color-editable-input input {
box-shadow: none !important;
font-size: 1rem !important;
color: rgb(244 244 245) !important;
font-weight: 500 !important;
letter-spacing: 0.1rem !important;
@apply text-center !rounded-lg !py-2 focus:outline-none;
}
.w-color-editable-input span {
font-size: 0.9rem !important;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
div[data-rmiz-modal-content] {
@apply bg-neutral-100 dark:bg-neutral-900;
}
progress::-webkit-progress-value {
@apply !bg-custom-500;
}
progress::-moz-progress-bar {
@apply !bg-custom-500;
}
.cm-editor * {
font-family: 'Jetbrains Mono', monospace !important;
}
.cm-editor {
@apply !bg-neutral-200/50;
}
.dark .cm-editor {
@apply !bg-neutral-800;
}
.cm-gutters {
@apply !bg-neutral-200;
}
.dark .cm-gutters {
@apply !bg-neutral-800;
}
.theme-red {
--color-custom-50: #ffebee;
--color-custom-100: #ffcdd2;
--color-custom-200: #ef9a9a;
--color-custom-300: #e57373;
--color-custom-400: #ef5350;
--color-custom-500: #f44336;
--color-custom-600: #e53935;
--color-custom-700: #d32f2f;
--color-custom-800: #c62828;
--color-custom-900: #b71c1c;
}
.theme-pink {
--color-custom-50: #fce4ec;
--color-custom-100: #f8bbd0;
--color-custom-200: #f48fb1;
--color-custom-300: #f06292;
--color-custom-400: #ec407a;
--color-custom-500: #e91e63;
--color-custom-600: #d81b60;
--color-custom-700: #c2185b;
--color-custom-800: #ad1457;
--color-custom-900: #880e4f;
}
.theme-purple {
--color-custom-50: #f3e5f5;
--color-custom-100: #e1bee7;
--color-custom-200: #ce93d8;
--color-custom-300: #ba68c8;
--color-custom-400: #ab47bc;
--color-custom-500: #9c27b0;
--color-custom-600: #8e24aa;
--color-custom-700: #7b1fa2;
--color-custom-800: #6a1b9a;
--color-custom-900: #4a148c;
}
.theme-deep-purple {
--color-custom-50: #ede7f6;
--color-custom-100: #d1c4e9;
--color-custom-200: #b39ddb;
--color-custom-300: #9575cd;
--color-custom-400: #7e57c2;
--color-custom-500: #673ab7;
--color-custom-600: #5e35b1;
--color-custom-700: #512da8;
--color-custom-800: #4527a0;
--color-custom-900: #311b92;
}
.theme-indigo {
--color-custom-50: #e8eaf6;
--color-custom-100: #c5cae9;
--color-custom-200: #9fa8da;
--color-custom-300: #7986cb;
--color-custom-400: #5c6bc0;
--color-custom-500: #3f51b5;
--color-custom-600: #3949ab;
--color-custom-700: #303f9f;
--color-custom-800: #283593;
--color-custom-900: #1a237e;
}
.theme-blue {
--color-custom-50: #e3f2fd;
--color-custom-100: #bbdefb;
--color-custom-200: #90caf9;
--color-custom-300: #64b5f6;
--color-custom-400: #42a5f5;
--color-custom-500: #2196f3;
--color-custom-600: #1e88e5;
--color-custom-700: #1976d2;
--color-custom-800: #1565c0;
--color-custom-900: #0d47a1;
}
.theme-light-blue {
--color-custom-50: #e1f5fe;
--color-custom-100: #b3e5fc;
--color-custom-200: #81d4fa;
--color-custom-300: #4fc3f7;
--color-custom-400: #29b6f6;
--color-custom-500: #03a9f4;
--color-custom-600: #039be5;
--color-custom-700: #0288d1;
--color-custom-800: #0277bd;
--color-custom-900: #01579b;
}
.theme-cyan {
--color-custom-50: #e0f7fa;
--color-custom-100: #b2ebf2;
--color-custom-200: #80deea;
--color-custom-300: #4dd0e1;
--color-custom-400: #26c6da;
--color-custom-500: #00bcd4;
--color-custom-600: #00acc1;
--color-custom-700: #0097a7;
--color-custom-800: #00838f;
--color-custom-900: #006064;
}
.theme-teal {
--color-custom-50: #e0f2f1;
--color-custom-100: #b2dfdb;
--color-custom-200: #80cbc4;
--color-custom-300: #4db6ac;
--color-custom-400: #26a69a;
--color-custom-500: #009688;
--color-custom-600: #00897b;
--color-custom-700: #00796b;
--color-custom-800: #00695c;
--color-custom-900: #004d40;
}
.theme-green {
--color-custom-50: #e8f5e9;
--color-custom-100: #c8e6c9;
--color-custom-200: #a5d6a7;
--color-custom-300: #81c784;
--color-custom-400: #66bb6a;
--color-custom-500: #4caf50;
--color-custom-600: #43a047;
--color-custom-700: #388e3c;
--color-custom-800: #2e7d32;
--color-custom-900: #1b5e20;
}
.theme-light-green {
--color-custom-50: #f1f8e9;
--color-custom-100: #dcedc8;
--color-custom-200: #c5e1a5;
--color-custom-300: #aed581;
--color-custom-400: #9ccc65;
--color-custom-500: #8bc34a;
--color-custom-600: #7cb342;
--color-custom-700: #689f38;
--color-custom-800: #558b2f;
--color-custom-900: #33691e;
}
.theme-lime {
--color-custom-50: #f9fbe7;
--color-custom-100: #f0f4c3;
--color-custom-200: #e6ee9c;
--color-custom-300: #dce775;
--color-custom-400: #d4e157;
--color-custom-500: #cddc39;
--color-custom-600: #c0ca33;
--color-custom-700: #afb42b;
--color-custom-800: #9e9d24;
--color-custom-900: #827717;
}
.theme-yellow {
--color-custom-50: #fffde7;
--color-custom-100: #fff9c4;
--color-custom-200: #fff59d;
--color-custom-300: #fff176;
--color-custom-400: #ffee58;
--color-custom-500: #ffeb3b;
--color-custom-600: #fdd835;
--color-custom-700: #fbc02d;
--color-custom-800: #f9a825;
--color-custom-900: #f57f17;
}
.theme-amber {
--color-custom-50: #fff8e1;
--color-custom-100: #ffecb3;
--color-custom-200: #ffe082;
--color-custom-300: #ffd54f;
--color-custom-400: #ffca28;
--color-custom-500: #ffc107;
--color-custom-600: #ffb300;
--color-custom-700: #ffa000;
--color-custom-800: #ff8f00;
--color-custom-900: #ff6f00;
}
.theme-orange {
--color-custom-50: #fff3e0;
--color-custom-100: #ffe0b2;
--color-custom-200: #ffcc80;
--color-custom-300: #ffb74d;
--color-custom-400: #ffa726;
--color-custom-500: #ff9800;
--color-custom-600: #fb8c00;
--color-custom-700: #f57c00;
--color-custom-800: #ef6c00;
--color-custom-900: #e65100;
}
.theme-deep-orange {
--color-custom-50: #fbe9e7;
--color-custom-100: #ffccbc;
--color-custom-200: #ffab91;
--color-custom-300: #ff8a65;
--color-custom-400: #ff7043;
--color-custom-500: #ff5722;
--color-custom-600: #f4511e;
--color-custom-700: #e64a19;
--color-custom-800: #d84315;
--color-custom-900: #bf360c;
}
.theme-brown {
--color-custom-50: #efebe9;
--color-custom-100: #d7ccc8;
--color-custom-200: #bcaaa4;
--color-custom-300: #a1887f;
--color-custom-400: #8d6e63;
--color-custom-500: #795548;
--color-custom-600: #6d4c41;
--color-custom-700: #5d4037;
--color-custom-800: #4e342e;
--color-custom-900: #3e2723;
}
.theme-grey {
--color-custom-50: #fafafa;
--color-custom-100: #f5f5f5;
--color-custom-200: #eeeeee;
--color-custom-300: #e0e0e0;
--color-custom-400: #bdbdbd;
--color-custom-500: #9e9e9e;
--color-custom-600: #757575;
--color-custom-700: #616161;
--color-custom-800: #424242;
--color-custom-900: #212121;
}
::global {
--toastify-color-light: var(--color-custom-500);
}
.Toastify__progress-bar--default {
background: var(--color-custom-500);
opacity: 1 !important;
}

View File

@@ -1,9 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import ReactDOM from 'react-dom/client'
import App from './App'
import 'react-tooltip/dist/react-tooltip.css'
import 'react-toastify/dist/ReactToastify.css'
import 'react-medium-image-zoom/dist/styles.css'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)

View File

@@ -1,256 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { Icon } from '@iconify/react'
function Calendar(): React.JSX.Element {
return (
<section className="flex w-full flex-col overflow-y-auto px-12">
<ModuleHeader
title="Calendar"
desc="Make sure you don't miss important event."
/>
<div className="mb-12 mt-8 flex min-h-0 w-full flex-1">
<aside className="flex h-full flex-col gap-8">
<section className="flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<div className="h-full w-full">
<div className="mb-6 flex items-center justify-between gap-2">
<button className="rounded-lg p-4 text-neutral-100 transition-all hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-left" className="text-2xl" />
</button>
<div className="text-lg font-semibold text-neutral-800 dark:text-neutral-100">
November 2023
</div>
<button className="rounded-lg p-4 text-neutral-100 transition-all hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
</div>
<div className="grid grid-cols-7 gap-4">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => (
<div
key={day}
className="flex items-center justify-center text-sm text-neutral-500"
>
{day}
</div>
))}
{Array(35)
.fill(0)
.map((_, index) =>
(() => {
const date = new Date()
const firstDay =
new Date(
date.getFullYear(),
date.getMonth(),
1
).getDay() - 1
const lastDate = new Date(
date.getFullYear(),
date.getMonth() + 1,
0
).getDate()
const lastDateOfPrevMonth =
new Date(
date.getFullYear(),
date.getMonth(),
0
).getDate() - 1
const actualIndex =
firstDay > index
? lastDateOfPrevMonth - firstDay + index + 2
: index - firstDay + 1 > lastDate
? index - lastDate - firstDay + 1
: index - firstDay + 1
return (
<div
key={index}
className={`relative isolate flex flex-col items-center gap-1 text-sm ${
firstDay > index || index - firstDay + 1 > lastDate
? 'text-neutral-300 dark:text-neutral-600'
: 'text-neutral-800 dark:text-neutral-100'
} ${
actualIndex === date.getDate()
? "font-semibold after:absolute after:left-1/2 after:top-1/2 after:z-[-1] after:h-10 after:w-10 after:-translate-x-1/2 after:-translate-y-6 after:rounded-md after:border after:border-custom-500 after:bg-custom-500/10 after:content-['']"
: ''
}`}
>
<span>{actualIndex}</span>
{(() => {
const randomTrue = Math.random() > 0.7
return randomTrue &&
!(
firstDay > index ||
index - firstDay + 1 > lastDate
) ? (
<div className="h-0.5 w-3 rounded-full bg-rose-500" />
) : (
''
)
})()}
</div>
)
})()
)}
</div>
</div>
</section>
<section className="flex w-full flex-col gap-4 overflow-y-auto rounded-lg bg-neutral-50 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h2 className="flex items-center gap-4 px-8 py-4 pt-8 text-sm font-semibold uppercase tracking-widest text-neutral-600 transition-all sm:px-12">
Categories
</h2>
<ul className="flex flex-col overflow-y-hidden pb-8 hover:overflow-y-scroll">
{[
['Design', 'bg-green-500'],
['Frontend', 'bg-blue-500'],
['Backend', 'bg-yellow-500'],
['Marketing', 'bg-red-500'],
['Sales', 'bg-purple-500'],
['Support', 'bg-pink-500']
].map(([name, color], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-200/50 dark:hover:bg-neutral-800">
<span
className={`block h-2 w-2 shrink-0 rounded-full ${color}`}
/>
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
</ul>
</section>
</aside>
<div className="ml-12 flex h-full flex-1 flex-col">
<div className="flex items-center justify-between">
<div className="flex items-center">
<button className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-left" className="text-2xl" />
</button>
<button className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
<div className="ml-4 text-3xl font-semibold text-neutral-800 dark:text-neutral-100">
Nov 20 - 26, 2023
</div>
<span className="ml-4 rounded-full bg-custom-500/20 px-4 py-1.5 text-sm font-semibold text-custom-500 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)]">
Week{' '}
{(() => {
const currentDate = new Date()
const startDate = new Date(currentDate.getFullYear(), 0, 1)
const days = Math.floor(
(currentDate - startDate) / (24 * 60 * 60 * 1000)
)
const weekNumber = Math.ceil(days / 7)
return weekNumber
})()}
</span>
</div>
<div className="flex items-center gap-4">
<button className="rounded-lg p-4 text-neutral-100 transition-all hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:search" className="text-2xl" />
</button>
<button className="flex items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] transition-all hover:bg-custom-600 dark:text-neutral-800">
<Icon icon="tabler:plus" className="text-xl" />
create
</button>
</div>
</div>
<div className="mt-4 flex h-full min-h-0 flex-1 flex-col">
<div className="mb-1.5 flex w-full">
<div className="flex w-20 shrink-0 flex-col items-center justify-center rounded-lg bg-neutral-50 py-4 text-neutral-500 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
GMT
<span className="text-xl">+8</span>
</div>
{Array(7)
.fill(0)
.map((_, index) => (
<div
key={index}
className={`ml-1.5 flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-50 py-4 text-neutral-500 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50 ${
index === 3 && 'bg-custom-500/20 text-custom-500'
}`}
>
<span>
{['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'][index]}
</span>
<span className="text-4xl font-semibold">{20 + index}</span>
</div>
))}
</div>
<div className="w-full flex-1 overflow-y-auto rounded-lg shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)]">
<div className="h-full w-full divide-y divide-neutral-300 dark:divide-neutral-700">
{Array(25)
.fill(0)
.map((_, hour) => (
<div key={hour} className="flex h-24">
<div className="relative h-full w-20 shrink-0 bg-neutral-50 text-neutral-100 dark:bg-neutral-800/50">
{hour !== 24 && (
<span className="absolute bottom-0 z-[9999] w-[90%] translate-y-1/2 bg-[#fafafa] pr-4 text-right dark:bg-[#1e1e1e]">
{hour + 1 > 12 ? hour + 1 - 12 : hour + 1}{' '}
{hour + 1 >= 12 ? 'PM' : 'AM'}
</span>
)}
</div>
{Array(7)
.fill(0)
.map((_, day) => (
<div
key={day}
className="relative w-full bg-neutral-50 dark:bg-neutral-800/50"
>
{day === 3 && hour === 1 && (
<div className="absolute left-0 top-0 z-[9999] ml-1.5 h-96 w-[90%] overflow-hidden rounded-r-md bg-neutral-50 dark:bg-neutral-900">
<div className="flex h-full w-full flex-col justify-between border-l-4 border-green-500 bg-green-500/20 px-3 py-4">
<div className="flex flex-col">
<Icon
icon="tabler:plane-departure"
className="mb-2 h-6 w-6 text-green-500"
/>
<span className="text-lg font-semibold text-green-500">
Flight to Taiwan
</span>
</div>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 text-lg font-medium">
SIN
<Icon
icon="tabler:arrow-down"
className="inline-block h-4 w-4 shrink-0 text-green-500"
/>
TPE
</div>
<span className="text-sm">FN: TR 898</span>
</div>
<div>
<span className="text-sm text-green-500">
01.00 AM - 05.00 AM
</span>
</div>
</div>
</div>
)}
</div>
))}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</section>
)
}
export default Calendar

View File

@@ -1,120 +0,0 @@
/* eslint-disable @typescript-eslint/no-throw-literal */
/* eslint-disable @typescript-eslint/indent */
import React, { Fragment, useEffect, useState } from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { toast } from 'react-toastify'
import Loading from '../../components/general/Loading'
import Error from '../../components/general/Error'
interface IChangeLogVersion {
version: string
date_range: [string, string]
entries: IChangeLogEntry[]
}
interface IChangeLogEntry {
id: string
feature: string
description: string
}
function Changelog(): React.ReactElement {
const [data, setData] = useState<'loading' | 'error' | IChangeLogVersion[]>(
'loading'
)
function updateChangeLogEntries(): void {
setData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/change-log/list`)
.then(async response => {
const data = await response.json()
setData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setData('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateChangeLogEntries()
}, [])
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-scroll px-8 sm:px-12">
<ModuleHeader
title="Change Log"
desc="All the changes made to this application will be listed here."
/>
{(() => {
switch (data) {
case 'loading':
return <Loading />
case 'error':
return <Error message="Failed to fetch data." />
default:
return (
<ul className="my-8 flex flex-col gap-4">
{data.map(entry => (
<li
key={entry.version}
className="flex flex-col gap-2 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<h3 className="mb-2 flex flex-col gap-2 text-2xl font-semibold sm:flex-row sm:items-end">
Ver. {entry.version}{' '}
<span className="mb-0.5 block text-sm">
(
{new Date(entry.date_range[0]).toLocaleDateString(
'en-US',
{
year: 'numeric',
month: 'short',
day: 'numeric'
}
)}{' '}
-{' '}
{new Date(entry.date_range[1]).toLocaleDateString(
'en-US',
{
year: 'numeric',
month: 'short',
day: 'numeric'
}
)}
)
</span>
</h3>
<ul className="flex list-inside list-disc flex-col gap-2 text-neutral-500 dark:text-neutral-400">
{entry.entries.map(subEntry => (
<li key={subEntry.id}>
<span className="font-semibold text-neutral-800 dark:text-neutral-100">
{subEntry.feature}:
</span>{' '}
<span
dangerouslySetInnerHTML={{
__html: subEntry.description.replace(
/<code>(.*?)<\/code>/,
`
<code class="inline-block rounded-md bg-neutral-200 p-1 px-1.5 font-['Jetbrains_Mono', text-sm shadow-[2px_2px_2px_rgba(0,0,0,0.05), dark:bg-neutral-800">$1</code>
`
)
}}
/>
</li>
))}
</ul>
</li>
))}
</ul>
)
}
})()}
</section>
)
}
export default Changelog

View File

@@ -1,445 +0,0 @@
/* eslint-disable @typescript-eslint/member-delimiter-style */
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import ActivityCalendar from 'react-activity-calendar'
import { Icon } from '@iconify/react'
import { Tooltip as ReactTooltip } from 'react-tooltip'
function HoursAndMinutesFromSeconds({
seconds
}: {
seconds: number
}): React.JSX.Element {
return (
<>
{Math.floor(seconds / 60) > 0 ? (
<>
{Math.floor(seconds / 60)}
<span className="pl-1 text-3xl font-normal text-neutral-500">h</span>
</>
) : (
''
)}{' '}
{Math.floor(seconds % 60) > 0 ? (
<>
{Math.floor(seconds % 60)}
<span className="pl-1 text-3xl font-normal text-neutral-500">m</span>
</>
) : (
''
)}{' '}
{seconds === 0 ? 'no time' : ''}
</>
)
}
export default function CodeTime(): React.JSX.Element {
const [activities, setActivities] = useState<Array<{
date: string
count: number
level: 0 | 1 | 2 | 3 | 4
}> | null>(null)
const [firstYear, setFirstYear] = useState(0)
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear())
const [stats, setStats] = useState<Record<string, number>>()
const [lastForProjects, setLastForProjects] = useState<
'24 hours' | '7 days' | '30 days'
>('24 hours')
const [lastForLanguages, setLastForLanguages] = useState<
'24 hours' | '7 days' | '30 days'
>('24 hours')
const [topProjects, setTopProjects] = useState<
Array<{ name: string; count: number }>
>([])
const [topLanguages, setTopLanguages] = useState<
Array<{ name: string; count: number }>
>([])
useEffect(() => {
fetch(
`${import.meta.env.VITE_API_HOST}/code-time/activities?year=` +
selectedYear
)
.then(async response => await response.json())
.then(data => {
setActivities(data.data.data)
setFirstYear(data.data.firstYear)
})
.catch(() => {})
fetch(`${import.meta.env.VITE_API_HOST}/code-time/statistics`)
.then(async response => await response.json())
.then(data => {
setStats(data.data)
})
.catch(() => {})
}, [])
useEffect(() => {
fetch(
`${import.meta.env.VITE_API_HOST}/code-time/languages?last=` +
lastForLanguages
)
.then(async response => await response.json())
.then(data => {
setTopLanguages(data.data)
})
.catch(() => {})
}, [lastForLanguages])
useEffect(() => {
fetch(
`${import.meta.env.VITE_API_HOST}/code-time/projects?last=` +
lastForProjects
)
.then(async response => await response.json())
.then(data => {
setTopProjects(data.data)
})
.catch(() => {})
}, [lastForProjects])
function switchSelectedYear(year: number): void {
setSelectedYear(year)
setActivities(null)
fetch(`${import.meta.env.VITE_API_HOST}/code-time/activities?year=` + year)
.then(async response => await response.json())
.then(data => {
setActivities(data.data.data.length > 0 ? data.data.data : null)
setFirstYear(data.data.firstYear)
})
.catch(() => {})
}
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-12">
<ModuleHeader
title="Code Time"
desc="See how much time you spend grinding code."
/>
<div className="mt-8 flex min-h-0 w-full flex-1 flex-col items-center overflow-y-auto">
{stats !== undefined && (
<div className="flex w-full flex-col gap-6">
<h1 className="mb-2 flex items-center gap-2 text-2xl font-semibold">
<Icon icon="tabler:chart-bar" className="text-3xl" />
<span className="ml-2">Statistics</span>
</h1>
<div className="flex items-center justify-between gap-4">
{Object.entries(stats).map(([key, value], index) => (
<div
key={key}
className="flex w-full flex-col items-start gap-2 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<div className="flex rounded-lg bg-neutral-200/70 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800">
<Icon
icon={
{
'Most time spent': 'tabler:coffee',
'Total time spent': 'tabler:clock',
'Average time spent': 'tabler:wave-saw-tool',
'Longest streak': 'tabler:flame',
'Current streak': 'tabler:flame'
}[key]!
}
className={`text-3xl ${
index === 3
? 'text-orange-300'
: 'text-neutral-500 dark:text-neutral-100'
}`}
/>
</div>
<div className="text-lg text-neutral-500">{key}</div>
<div className="text-4xl font-semibold text-neutral-800">
{index < 3 ? (
<HoursAndMinutesFromSeconds seconds={value} />
) : (
<>
{value}
<span className="pl-1 text-3xl font-normal text-neutral-500">
days
</span>
</>
)}
</div>
</div>
))}
</div>
</div>
)}
<div className="mt-16 flex w-full flex-col gap-6">
<h1 className="mb-2 flex items-center gap-2 text-2xl font-semibold">
<Icon icon="tabler:activity" className="text-3xl" />
<span className="ml-2">Activities Calendar</span>
</h1>
<div className="flex items-center justify-between">
<div
className={`flex flex-1 items-center ${
Array.isArray(activities) ? 'justify-start' : 'justify-center'
}`}
>
{Array.isArray(activities) ? (
<ActivityCalendar
data={activities}
blockSize={14}
blockMargin={6}
labels={{
totalCount: `${
Math.floor(
activities.reduce((a, b) => a + b.count, 0) / 60
) > 0
? `${Math.floor(
activities.reduce((a, b) => a + b.count, 0) / 60
)} hours`
: ''
} ${
Math.floor(
activities.reduce((a, b) => a + b.count, 0) % 60
) > 0
? `${Math.floor(
activities.reduce((a, b) => a + b.count, 0) % 60
)} minutes`
: ''
} ${
activities.reduce((a, b) => a + b.count, 0) === 0
? 'no time'
: ''
} spent on {{year}}`
}}
renderBlock={(block, activity) =>
React.cloneElement(block, {
'data-tooltip-id': 'react-tooltip',
'data-tooltip-html': `${
Math.floor(activity.count / 60) > 0
? `${Math.floor(activity.count / 60)} hours`
: ''
} ${
Math.floor(activity.count % 60) > 0
? `${Math.floor(activity.count % 60)} minutes`
: ''
} ${activity.count === 0 ? 'no time' : ''} spent on ${
activity.date
}`.trim()
})
}
theme={{
// dark: ['rgb(38, 38, 38)', 'rgb(20, 184, 166)'],
dark: ['rgb(229, 229, 229)', 'rgb(20, 184, 166)']
}}
/>
) : (
<Icon icon="svg-spinners:180-ring" className="text-4xl" />
)}
</div>
<ReactTooltip id="react-tooltip" />
{firstYear && (
<div className="flex flex-col gap-2">
{Array(new Date().getFullYear() - firstYear + 1)
.fill(0)
.map((_, index) => (
<button
key={index}
onClick={() => {
switchSelectedYear(firstYear + index)
}}
className={`flex items-start gap-2 rounded-lg p-4 px-8 sm:px-12 font-medium ${
selectedYear === firstYear + index
? 'bg-neutral-200 font-semibold text-neutral-800 dark:bg-neutral-700/50 dark:text-neutral-100'
: 'text-neutral-400 hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50'
}`}
>
<span>{firstYear + index}</span>
</button>
))}
</div>
)}
</div>
</div>
<div className="mt-16 flex w-full flex-col gap-6">
<div className="flex w-full items-center justify-between">
<h1 className="mb-2 flex items-center gap-2 text-2xl font-semibold">
<Icon icon="tabler:clipboard" className="text-3xl" />
<span className="ml-2">
Projects You&apos;ve Spent Most Time Doing
</span>
</h1>
<div className="flex items-center gap-2">
<p className="font-medium tracking-wider">in the last</p>
<div className="flex gap-2 rounded-lg p-2">
{['24 hours', '7 days', '30 days'].map((last, index) => (
<button
key={index}
onClick={() => {
setLastForProjects(
last as '24 hours' | '7 days' | '30 days'
)
}}
className={`rounded-md p-4 px-6 tracking-wide ${
lastForProjects === last
? 'bg-neutral-200 font-semibold text-neutral-800 dark:bg-neutral-700/50 dark:text-neutral-100'
: 'text-neutral-400 hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50'
}`}
>
{last}
</button>
))}
</div>
</div>
</div>
<div className="flex w-full">
{Object.keys(topProjects).length > 0 &&
Object.entries(topProjects)
.slice(0, 5)
.map(([key, value], index) => (
<div
className={`h-6 border ${index === 0 && 'rounded-l-lg'} ${
index === 4 && 'rounded-r-lg'
} ${
[
'bg-red-500/20 border-red-500',
'bg-orange-500/20 border-orange-500',
'bg-yellow-500/20 border-yellow-500',
'bg-blue-500/20 border-blue-500',
'bg-emerald-500/20 border-emerald-500'
][index]
}`}
key={key}
style={{
width: `${Math.round(
(value /
Object.entries(topProjects)
.slice(0, 5)
.reduce((a, b) => a + b[1], 0)) *
100
)}%`
}}
></div>
))}
</div>
<ul className="flex flex-col gap-4">
{Object.keys(topProjects).length > 0 &&
Object.entries(topProjects)
.slice(0, 5)
.map(([key, value], index) => (
<li
key={key}
className="relative flex items-center justify-between gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<div className="flex items-center gap-4 text-lg font-medium text-neutral-800">
<div
className={`h-4 w-4 rounded-md border ${
[
'bg-red-500/20 border-red-500',
'bg-orange-500/20 border-orange-500',
'bg-yellow-500/20 border-yellow-500',
'bg-blue-500/20 border-blue-500',
'bg-emerald-500/20 border-emerald-500'
][index]
} rounded-full`}
></div>
{key}
</div>
<div className="text-3xl font-semibold text-neutral-800">
<HoursAndMinutesFromSeconds seconds={value} />
</div>
</li>
))}
</ul>
</div>
<div className="mb-6 mt-16 flex w-full flex-col gap-6">
<div className="flex w-full items-center justify-between">
<h1 className="mb-2 flex items-center gap-2 text-2xl font-semibold">
<Icon icon="tabler:code" className="text-3xl" />
<span className="ml-2">
Languages You&apos;ve Spent Most Time Using
</span>
</h1>
<div className="flex items-center gap-2">
<p className="font-medium tracking-wider">in the last</p>
<div className="flex gap-2 rounded-lg p-2">
{['24 hours', '7 days', '30 days'].map((last, index) => (
<button
key={index}
onClick={() => {
setLastForLanguages(
last as '24 hours' | '7 days' | '30 days'
)
}}
className={`rounded-md p-4 px-6 tracking-wide hover:bg-neutral-700/50 ${
lastForLanguages === last
? 'bg-neutral-200 font-semibold text-neutral-800 dark:bg-neutral-700/50 dark:text-neutral-100'
: 'text-neutral-400 hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50'
}`}
>
{last}
</button>
))}
</div>
</div>
</div>
<div className="flex w-full">
{Object.keys(topLanguages).length > 0 &&
Object.entries(topLanguages)
.slice(0, 5)
.map(([key, value], index) => (
<div
className={`h-6 border ${index === 0 && 'rounded-l-lg'} ${
index === 4 && 'rounded-r-lg'
} ${
[
'bg-red-500/20 border-red-500',
'bg-orange-500/20 border-orange-500',
'bg-yellow-500/20 border-yellow-500',
'bg-blue-500/20 border-blue-500',
'bg-emerald-500/20 border-emerald-500'
][index]
}`}
key={key}
style={{
width: `${Math.round(
(value /
Object.entries(topLanguages)
.slice(0, 5)
.reduce((a, b) => a + b[1], 0)) *
100
)}%`
}}
></div>
))}
</div>
<ul className="flex flex-col gap-4">
{Object.keys(topLanguages).length > 0 &&
Object.entries(topLanguages)
.slice(0, 5)
.map(([key, value], index) => (
<li
key={key}
className="relative flex items-center justify-between gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<div className="flex items-center gap-4 text-lg font-medium text-neutral-800">
<div
className={`h-4 w-4 rounded-md border ${
[
'bg-red-500/20 border-red-500',
'bg-orange-500/20 border-orange-500',
'bg-yellow-500/20 border-yellow-500',
'bg-blue-500/20 border-blue-500',
'bg-emerald-500/20 border-emerald-500'
][index]
} rounded-full`}
></div>
{key}
</div>
<div className="text-3xl font-semibold text-neutral-800">
<HoursAndMinutesFromSeconds seconds={value} />
</div>
</li>
))}
</ul>
</div>
</div>
</section>
)
}

View File

@@ -1,66 +0,0 @@
import React, { useContext } from 'react'
import { Icon } from '@iconify/react'
import StorageStatus from './modules/StorageStatus'
import CodeTime from './modules/CodeTime'
import WalletBalance from './modules/WalletBalance'
import TodaysEvent from './modules/TodaysEvent'
import Calendar from './modules/Calendar'
import TodoList from './modules/TodoList'
import {
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Filler
} from 'chart.js'
import ModuleHeader from '../../components/general/ModuleHeader'
import { AuthContext } from '../../providers/AuthProvider'
ChartJS.register(
ArcElement,
Tooltip,
Legend,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Filler
)
function Dashboard(): React.JSX.Element {
const { userData } = useContext(AuthContext)
return (
<section className="flex w-full flex-col overflow-y-auto px-12">
<div className="mb-8 flex w-full flex-col">
<ModuleHeader
title="Dashboard"
desc={
<>
Good to see you here,{' '}
<span className="text-custom-500">{userData?.name}</span>!
</>
}
/>
<div className="mt-6 grid w-full grid-cols-4 grid-rows-3 gap-6">
<StorageStatus />
<CodeTime />
<TodaysEvent />
<WalletBalance />
<TodoList />
<Calendar />
</div>
</div>
</section>
)
}
export default Dashboard

View File

@@ -1,87 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import { Icon } from '@iconify/react'
export default function Calendar(): React.JSX.Element {
return (
<section className="col-span-2 row-span-1 flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:calendar" className="text-2xl" />
<span className="ml-2">Calendar</span>
</h1>
<div className="h-full w-full">
<div className="mb-6 flex items-center justify-between">
<button className="text-neutral-100 rounded-lg p-4 transition-all hover:bg-neutral-200 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-left" className="text-2xl" />
</button>
<div className="text-lg font-semibold text-neutral-800 dark:text-neutral-100">
November 2023
</div>
<button className="text-neutral-100 rounded-lg p-4 transition-all hover:bg-neutral-200 dark:hover:bg-neutral-700/50">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
</div>
<div className="grid grid-cols-7 gap-4">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => (
<div
key={day}
className="text-neutral-100 flex items-center justify-center text-sm"
>
{day}
</div>
))}
{Array(35)
.fill(0)
.map((_, index) =>
(() => {
const date = new Date()
const firstDay =
new Date(date.getFullYear(), date.getMonth(), 1).getDay() - 1
const lastDate = new Date(
date.getFullYear(),
date.getMonth() + 1,
0
).getDate()
const lastDateOfPrevMonth =
new Date(date.getFullYear(), date.getMonth(), 0).getDate() - 1
const actualIndex =
firstDay > index
? lastDateOfPrevMonth - firstDay + index + 2
: index - firstDay + 1 > lastDate
? index - lastDate - firstDay + 1
: index - firstDay + 1
return (
<div
key={index}
className={`relative isolate flex flex-col items-center gap-1 text-sm ${
firstDay > index || index - firstDay + 1 > lastDate
? 'text-neutral-300'
: 'text-neutral-800 dark:text-neutral-100'
} ${
actualIndex === date.getDate()
? "after:absolute after:left-1/2 after:top-1/2 after:z-[-1] after:h-10 after:w-10 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-md after:border after:border-custom-500 after:bg-custom-500/10 after:content-['']"
: ''
}`}
>
<span>{actualIndex}</span>
{(() => {
const randomTrue = Math.random() > 0.7
return randomTrue &&
!(
firstDay > index || index - firstDay + 1 > lastDate
) ? (
<div className="h-0.5 w-3 rounded-full bg-rose-500" />
) : (
''
)
})()}
</div>
)
})()
)}
</div>
</div>
</section>
)
}

View File

@@ -1,132 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import { Icon } from '@iconify/react'
import { type ScriptableContext } from 'chart.js'
import { Line } from 'react-chartjs-2'
function msToTime(ms: number): string {
const seconds = (ms / 1000).toFixed(1)
const minutes = (ms / (1000 * 60)).toFixed(1)
const hours = (ms / (1000 * 60 * 60)).toFixed(1)
const days = (ms / (1000 * 60 * 60 * 24)).toFixed(1)
if (parseFloat(seconds) < 60) return seconds + ' Sec'
else if (parseFloat(minutes) < 60) return minutes + ' Min'
else if (parseFloat(hours) < 24) return hours + ' Hrs'
else return days + ' Days'
}
const raw = [
['2023-11-23T00:00:00Z', 12180000],
['2023-11-22T00:00:00Z', 7620000],
['2023-11-21T00:00:00Z', 16200000],
['2023-11-20T00:00:00Z', 18360000],
['2023-11-18T00:00:00Z', 2940000],
['2023-11-17T00:00:00Z', 16020000],
['2023-11-16T00:00:00Z', 2040000],
['2023-11-15T00:00:00Z', 3480000],
['2023-11-14T00:00:00Z', 7200000],
['2023-11-11T00:00:00Z', 5580000],
['2023-11-10T00:00:00Z', 8640000],
['2023-11-08T00:00:00Z', 480000],
['2023-11-06T00:00:00Z', 420000],
['2023-11-05T00:00:00Z', 9960000],
['2023-11-04T00:00:00Z', 13620000],
['2023-11-03T00:00:00Z', 5940000],
['2023-11-02T00:00:00Z', 13500000],
['2023-11-01T00:00:00Z', 11640000],
['2023-10-31T00:00:00Z', 16500000],
['2023-10-29T00:00:00Z', 13800000],
['2023-10-28T00:00:00Z', 22800000],
['2023-10-27T00:00:00Z', 7380000],
['2023-10-26T00:00:00Z', 8820000],
['2023-10-25T00:00:00Z', 960000],
['2023-10-24T00:00:00Z', 10440000]
].reverse()
const data2 = {
labels: raw.map(([date]) =>
new Date(date).toDateString().split(' ').slice(1, 3).join(' ')
),
datasets: [
{
label: 'Code time',
data: raw.map(([, value]) => (value as number) / 3600000),
backgroundColor: (context: ScriptableContext<'line'>) => {
const ctx = context.chart.ctx
const gradient = ctx.createLinearGradient(0, 0, 0, 250)
gradient.addColorStop(0, 'rgba(20,184,166,0.2)')
gradient.addColorStop(1, 'rgba(20,184,166,0)')
return gradient
},
fill: 'origin',
borderColor: 'rgba(20, 184, 166, 1)',
borderWidth: 1,
tension: 0.4
}
]
}
const options2 = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context: any) {
let label = context.dataset.label || ''
if (label) {
label += ': '
}
if (context.parsed.y !== null) {
label += msToTime(context.parsed.y * 3600000)
}
return label
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
type: 'time',
callback: function (label: number) {
return Math.round(label) + 'h'
}
},
grid: {
drawOnChartArea: false
},
border: {
color: 'rgba(163, 163, 163, 0.5)'
}
},
x: {
grid: {
drawOnChartArea: false
},
border: {
color: 'rgba(163, 163, 163, 0.5)'
}
}
}
}
export default function CodeTime(): React.JSX.Element {
return (
<section className="col-span-2 flex h-full w-full flex-col gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:chart-line" className="text-2xl" />
<span className="ml-2">Code Time</span>
</h1>
<div className="flex h-72 w-full items-center justify-center">
<Line data={data2} options={options2} />
</div>
</section>
)
}

View File

@@ -1,73 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Icon } from '@iconify/react'
import React from 'react'
import { Doughnut } from 'react-chartjs-2'
const data = {
labels: ['Images', 'Videos', 'Musics', 'Documents'],
datasets: [
{
label: 'Storage occupation',
data: [19, 12, 3, 5],
backgroundColor: [
'rgba(244, 63, 94, 0.2)',
'rgba(245 ,158, 11, 0.2)',
'rgba(59, 130, 246, 0.2)',
'rgba(34, 197, 94, 0.2)'
],
borderColor: [
'rgba(244, 63, 94, 1)',
'rgba(245, 158, 11, 1)',
'rgba(59, 130, 246, 1)',
'rgba(34, 197, 94, 1)'
],
borderWidth: 1
}
]
}
const options = {
plugins: {
tooltip: {
callbacks: {
label: function (context: any) {
let label = context.dataset.label || ''
if (label) {
label += ': '
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(context.parsed / 100)
}
return label
}
}
},
legend: {
labels: {
color: 'rgb(18, 18, 18)'
},
position: 'bottom' as const
}
}
}
export default function StorageStatus(): React.JSX.Element {
return (
<section className="col-span-1 flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:server" className="text-2xl" />
<span className="ml-2">Storage Status</span>
</h1>
<div className="flex h-full w-full items-center justify-center">
<Doughnut data={data} options={options} />
</div>
<p className="text-center text-lg font-medium">520 GB of 1 TB used</p>
</section>
)
}

View File

@@ -1,42 +0,0 @@
import React from 'react'
import { Icon } from '@iconify/react'
export default function TodaysEvent(): React.JSX.Element {
return (
<section className="col-span-1 flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:calendar" className="text-2xl" />
<span className="ml-2">Today&apos;s Event</span>
</h1>
<ul className="flex h-full flex-col gap-4">
<li className="flex flex-1 items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="h-full w-1.5 rounded-full bg-rose-500" />
<div className="flex w-full flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Coldplay&apos;s concert
</div>
<div className="text-sm text-neutral-400">8:00 PM</div>
</div>
</li>
<li className="flex flex-1 items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="h-full w-1.5 rounded-full bg-purple-500" />
<div className="flex w-full flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Meeting with client
</div>
<div className="text-sm text-neutral-400">10:00 PM</div>
</div>
</li>
<li className="flex flex-1 items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="h-full w-1.5 rounded-full bg-purple-500" />
<div className="flex w-full flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Deadline for project
</div>
<div className="text-sm text-neutral-400">11:59 PM</div>
</div>
</li>
</ul>
</section>
)
}

View File

@@ -1,48 +0,0 @@
import React from 'react'
import { Icon } from '@iconify/react'
export default function TodoList(): React.JSX.Element {
return (
<section className="col-span-2 row-span-2 flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:clipboard-list" className="text-2xl" />
<span className="ml-2">Todo List</span>
</h1>
<ul className="flex flex-col gap-4">
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-indigo-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Buy groceries
</div>
<div className="text-sm text-rose-500">
10:00 AM, 23 Nov 2023 (overdue 8 hours)
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-400 transition-all hover:border-orange-500" />
</li>
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-orange-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Do homework
</div>
<div className="text-sm text-neutral-400">
00:00 AM, 31 Jan 2024
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-400 transition-all hover:border-orange-500" />
</li>
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-orange-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] dark:bg-neutral-800">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Start doing revision for SPM Sejarah
</div>
<div className="text-sm text-neutral-400">
00:00 AM, 31 Jan 2024
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-400 transition-all hover:border-orange-500" />
</li>
</ul>
</section>
)
}

View File

@@ -1,57 +0,0 @@
import { Icon } from '@iconify/react'
import React from 'react'
export default function WalletBalance(): React.JSX.Element {
return (
<section className="col-span-2 row-span-1 flex w-full flex-col gap-4 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-xl font-semibold">
<Icon icon="tabler:wallet" className="text-2xl" />
<span className="ml-2">Wallet Balance</span>
</h1>
<ul className="flex flex-col gap-4">
<li className="flex items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 pl-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] transition-all hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700/50">
<div className="flex items-center gap-4">
<Icon icon="tabler:cash" className="h-6 w-6" />
<div className="flex flex-col">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Cash
</div>
<div className="text-sm text-neutral-500">RM 520.00</div>
</div>
</div>
<button className="rounded-lg p-4 text-neutral-400 transition-all">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
</li>
<li className="flex items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 pl-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] transition-all hover:bg-neutral-200 dark:bg-neutral-800">
<div className="flex items-center gap-4">
<Icon icon="tabler:device-mobile" className="h-6 w-6" />
<div className="flex flex-col">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Touch N&apos; Go e-Wallet
</div>
<div className="text-sm text-neutral-500">RM 128.00</div>
</div>
</div>
<button className="rounded-lg p-4 text-neutral-400 transition-all">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
</li>
<li className="flex items-center justify-between gap-4 rounded-lg bg-neutral-100 p-4 pl-6 shadow-[4px_4px_10px_rgba(0,0,0,0.1)] transition-all hover:bg-neutral-200 dark:bg-neutral-800">
<div className="flex items-center gap-4">
<Icon icon="tabler:building-bank" className="h-6 w-6" />
<div className="flex flex-col">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
Bank Account
</div>
<div className="text-sm text-neutral-500">RM 12,487.00</div>
</div>
</div>
<button className="rounded-lg p-4 text-neutral-400 transition-all">
<Icon icon="tabler:chevron-right" className="text-2xl" />
</button>
</li>
</ul>
</section>
)
}

View File

@@ -1,63 +0,0 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { Link } from 'react-router-dom'
function CardSet(): React.JSX.Element {
return (
<section className="relative flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-y-auto px-12">
<div className="flex flex-col gap-1">
<Link
to="/flashcards"
className="mb-2 flex w-min items-center gap-2 rounded-lg p-2 pl-0 pr-4 text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-100"
>
<Icon icon="tabler:chevron-left" className="text-xl" />
<span className="whitespace-nowrap text-lg font-medium">Go back</span>
</Link>
<div className="flex items-center justify-between">
<h1 className="flex items-center text-3xl font-semibold ">
<span className="ml-4 rounded-full bg-custom-500/20 px-4 py-1.5 text-sm font-semibold text-custom-500">
40 cards
</span>
</h1>
<div className="flex items-center justify-center gap-2">
<button className="rounded-lg p-4 text-neutral-100 transition-all hover:bg-neutral-800 hover:text-neutral-100">
<Icon icon="tabler:border-corners" className="text-2xl" />
</button>
<button className="rounded-lg p-4 text-neutral-100 transition-all hover:bg-neutral-800 hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="text-2xl" />
</button>
</div>
</div>
</div>
<div className="flex w-full flex-1 flex-col items-center justify-center">
<div className="flex h-1/2 w-3/5 items-center justify-center gap-4">
<button className="flex h-full shrink-0 items-center justify-center p-4">
<Icon icon="tabler:chevron-left" className="text-3xl" />
</button>
<div className="stack h-full w-full">
<div className="card h-full bg-custom-500 text-neutral-800 shadow-md">
<div className="card-body flex h-full flex-col">
<h2 className="card-title text-custom-700">#1</h2>
<div className="flex w-full flex-1 flex-col items-center justify-center text-3xl">
</div>
</div>
</div>
<div className="card h-full bg-custom-700 text-neutral-800 !opacity-100 shadow"></div>
<div className="card h-full bg-custom-900 text-neutral-800 !opacity-100 shadow-sm"></div>
</div>
<button className="flex h-full shrink-0 items-center justify-center p-4">
<Icon icon="tabler:chevron-right" className="text-3xl" />
</button>
</div>
<button className="mt-12 flex w-1/2 items-center justify-center gap-2 rounded-lg bg-neutral-200 p-4 text-lg font-medium shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800">
<Icon icon="tabler:bulb" className="text-2xl" />
Show answer
</button>
</div>
</section>
)
}
export default CardSet

View File

@@ -1,90 +0,0 @@
import { faker } from '@faker-js/faker'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { Link } from 'react-router-dom'
import ModuleHeader from '../../components/general/ModuleHeader'
export default function Flashcards(): React.JSX.Element {
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-12">
<ModuleHeader
title="Flashcards"
desc="Memorizing could be a pain, but not with flashcards."
/>
<div className="mt-8 flex min-h-0 w-full flex-1 flex-col">
<search className="flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
placeholder="Search flashcard sets ..."
className="w-full bg-transparent text-neutral-500 placeholder:text-neutral-400 focus:outline-none"
/>
</search>
<div className="mt-6 grid w-full grid-cols-3 gap-6 overflow-y-auto pb-12">
{Array(5)
.fill(0)
.map((_, index) => (
<Link
to={`/flashcards/${index}`}
key={index}
className="relative flex flex-col justify-start gap-6 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-200/50 dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-neutral-400">
{faker.datatype.number(100)} cards
</p>
<div className="text-xl font-medium text-neutral-800 dark:text-neutral-100">
{faker.commerce.productName()}
</div>
</div>
<div className="flex flex-col gap-2">
<progress
className="progress h-2 w-full rounded-lg bg-neutral-200 dark:bg-neutral-700"
value={faker.datatype.number(100)}
max="100"
></progress>
<p className="text-sm font-medium text-neutral-400">
{faker.datatype.number(100)}% complete
</p>
</div>
<button className="absolute right-4 top-4 rounded-md p-2 text-neutral-500 hover:bg-neutral-700/30 hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="h-5 w-5" />
</button>
</Link>
))}
{Array(4)
.fill(0)
.map((_, index) => (
<Link
to={`/idea-box/${index}`}
key={index}
className="relative flex flex-col justify-start gap-6 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-200/50 dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-neutral-400">
{faker.datatype.number(100)} cards
</p>
<div className="text-xl font-medium text-neutral-800 dark:text-neutral-100">
{faker.commerce.productName()}
</div>
</div>
<div className="text-custom-500 flex flex-1 items-center justify-center gap-2">
<Icon icon="tabler:check" className="h-5 w-5" />
<p className="font-medium">Done</p>
</div>
<button className="absolute right-4 top-4 rounded-md p-2 text-neutral-500 hover:bg-neutral-700/30 hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="h-5 w-5" />
</button>
</Link>
))}
<div className="relative flex h-full flex-col items-center justify-center gap-4 rounded-lg border-2 border-dashed border-neutral-400 p-12">
<Icon icon="tabler:plus" className="h-8 w-8 text-neutral-400" />
<div className="text-xl font-semibold text-neutral-400">
Create new set
</div>
</div>
</div>
</div>
</section>
)
}

View File

@@ -1,85 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { Link } from 'react-router-dom'
import { type IIdeaBoxContainer } from '../../..'
import HamburgerMenu from '../../../../../components/general/HamburgerMenu'
import MenuItem from '../../../../../components/general/HamburgerMenu/MenuItem'
function ContainerGridItem({
container,
setCreateContainerModalOpen,
setExistedData,
setDeleteContainerConfirmationModalOpen
}: {
container: IIdeaBoxContainer
setCreateContainerModalOpen: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: React.Dispatch<React.SetStateAction<IIdeaBoxContainer | null>>
setDeleteContainerConfirmationModalOpen: React.Dispatch<
React.SetStateAction<boolean>
>
}): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-start gap-6 rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-100 dark:bg-neutral-800/50 dark:hover:bg-neutral-800/70">
<div
className="rounded-lg p-4"
style={{
backgroundColor: container.color + '20'
}}
>
<Icon
icon={container.icon}
className="h-8 w-8"
style={{
color: container.color
}}
/>
</div>
<div className="text-center text-2xl font-medium text-neutral-800 dark:text-neutral-100">
{container.name}
</div>
<div className="mt-auto flex items-center gap-4">
<div className="flex items-center gap-2">
<Icon icon="tabler:article" className="h-5 w-5 text-neutral-500" />
<span className="text-neutral-500">{container.text_count}</span>
</div>
<div className="flex items-center gap-2">
<Icon icon="tabler:link" className="h-5 w-5 text-neutral-500" />
<span className="text-neutral-500">{container.link_count}</span>
</div>
<div className="flex items-center gap-2">
<Icon icon="tabler:photo" className="h-5 w-5 text-neutral-500" />
<span className="text-neutral-500">{container.image_count}</span>
</div>
</div>
<Link
to={`/idea-box/${container.id}`}
className="absolute left-0 top-0 h-full w-full"
/>
<HamburgerMenu position="absolute right-4 top-4 overscroll-contain">
<MenuItem
onClick={() => {
setExistedData(container)
setCreateContainerModalOpen('update')
}}
icon="tabler:edit"
text="Edit"
/>
<MenuItem
onClick={() => {
setExistedData(container)
setDeleteContainerConfirmationModalOpen(true)
}}
icon="tabler:trash"
text="Delete"
isRed
/>
</HamburgerMenu>
</div>
)
}
export default ContainerGridItem

View File

@@ -1,53 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import { type IIdeaBoxContainer } from '../..'
import ContainerGridItem from './components/ContainerGridItem'
import { Icon } from '@iconify/react/dist/iconify.js'
function Container({
filteredList,
setCreateContainerModalOpen,
setExistedData,
setDeleteContainerConfirmationModalOpen
}: {
filteredList: IIdeaBoxContainer[]
setCreateContainerModalOpen: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: React.Dispatch<React.SetStateAction<IIdeaBoxContainer | null>>
setDeleteContainerConfirmationModalOpen: React.Dispatch<
React.SetStateAction<boolean>
>
}): React.ReactElement {
return (
<div className="mt-6 grid w-full grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] gap-6 overflow-y-auto px-3 pb-12">
{filteredList.map(container => (
<ContainerGridItem
key={container.id}
container={container}
setCreateContainerModalOpen={setCreateContainerModalOpen}
setExistedData={setExistedData}
setDeleteContainerConfirmationModalOpen={
setDeleteContainerConfirmationModalOpen
}
/>
))}
<button
onClick={() => {
setCreateContainerModalOpen('create')
}}
className="relative flex h-full flex-col items-center justify-center gap-6 rounded-lg border-2 border-dashed border-neutral-400 p-8 hover:bg-neutral-200 dark:border-neutral-700 dark:hover:bg-neutral-800/20"
>
<Icon
icon="tabler:cube-plus"
className="h-8 w-8 text-neutral-400 dark:text-neutral-100"
/>
<div className="text-xl font-semibold text-neutral-400 dark:text-neutral-100">
Create container
</div>
</button>
</div>
)
}
export default Container

View File

@@ -1,90 +0,0 @@
/* eslint-disable multiline-ternary */
import React, { useState } from 'react'
import Modal from '../../../../../components/general/Modal'
import { type IIdeaBoxContainer } from '../../..'
import { toast } from 'react-toastify'
import { Icon } from '@iconify/react/dist/iconify.js'
function DeleteContainerConfirmationModal({
isOpen,
closeModal,
containerDetails,
updateContainerList
}: {
isOpen: boolean
closeModal: () => void
containerDetails: IIdeaBoxContainer | null
updateContainerList: () => void
}): React.ReactElement {
const [loading, setLoading] = useState(false)
function deleteContainer(): void {
if (containerDetails === null) return
setLoading(true)
fetch(
`${import.meta.env.VITE_API_HOST}/idea-box/container/delete/${
containerDetails.id
}`,
{
method: 'DELETE'
}
)
.then(async res => {
const data = await res.json()
if (res.ok) {
toast.info("Uhh, hopefully you truly didn't need that container.")
closeModal()
updateContainerList()
return data
} else {
throw new Error(data.message)
}
})
.catch(err => {
toast.error("Oops! Couldn't delete the container. Please try again.")
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
return (
<Modal isOpen={isOpen}>
<h1 className="text-2xl font-semibold">
Are you sure you want to delete {containerDetails?.name}?
</h1>
<p className="mt-2 text-neutral-500">
This will delete the container and all the ideas inside it. This action
is irreversible!
</p>
<div className="mt-8 flex w-full justify-around gap-2">
<button
onClick={closeModal}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-800 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-neutral-700"
>
Cancel
</button>
<button
disabled={loading}
onClick={deleteContainer}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-red-600"
>
{loading ? (
<>
<span className="small-loader-light"></span>
</>
) : (
<>
<Icon icon="tabler:trash" className="h-5 w-5" />
DELETE
</>
)}
</button>
</div>
</Modal>
)
}
export default DeleteContainerConfirmationModal

View File

@@ -1,234 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import Modal from '../../../../../components/general/Modal'
import { Icon } from '@iconify/react/dist/iconify.js'
import ColorPickerModal from '../../../../../components/general/ColorPickerModal'
import { toast } from 'react-toastify'
import { type IIdeaBoxContainer } from '../../..'
import { useDebounce } from '@uidotdev/usehooks'
import IconInput from '../../../../../components/general/IconSelector/IconInput'
import Input from '../../../../../components/general/Input'
function ModifyContainerModal({
openType,
setOpenType,
updateContainerList,
existedData
}: {
openType: 'create' | 'update' | null
setOpenType: React.Dispatch<React.SetStateAction<'create' | 'update' | null>>
updateContainerList: () => void
existedData: IIdeaBoxContainer | null
}): React.ReactElement {
const [loading, setLoading] = useState(false)
const [containerName, setContainerName] = useState('')
const [containerColor, setContainerColor] = useState('#FFFFFF')
const [containerIcon, setContainerIcon] = useState('tabler:cube')
const [colorPickerOpen, setColorPickerOpen] = useState(false)
const innerOpenType = useDebounce(openType, openType === null ? 300 : 0)
function updateContainerName(e: React.ChangeEvent<HTMLInputElement>): void {
setContainerName(e.target.value)
}
function updateContainerColor(e: React.ChangeEvent<HTMLInputElement>): void {
setContainerColor(e.target.value)
}
function updateContainerIcon(e: React.ChangeEvent<HTMLInputElement>): void {
setContainerIcon(e.target.value)
}
function onSubmitButtonClick(): void {
if (
containerName.trim().length === 0 ||
containerColor.trim().length === 0 ||
containerIcon.trim().length === 0
) {
toast.error('Please fill in all the fields.')
return
}
setLoading(true)
const container = {
name: containerName.trim(),
color: containerColor.trim(),
icon: containerIcon.trim()
}
fetch(
`${import.meta.env.VITE_API_HOST}/idea-box/container/${innerOpenType}` +
(innerOpenType === 'update' ? `/${existedData!.id}` : ''),
{
method: innerOpenType === 'create' ? 'PUT' : 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(container)
}
)
.then(async res => {
const data = await res.json()
if (res.status !== 200) {
throw data.message
}
toast.success(
{
create: 'Yay! Container created. Time to fill it up.',
update: 'Yay! Container updated.'
}[innerOpenType!]
)
setOpenType(null)
updateContainerList()
})
.catch(err => {
toast.error(
{
create: "Oops! Couldn't create the container. Please try again.",
update: "Oops! Couldn't update the container. Please try again."
}[innerOpenType!]
)
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
if (innerOpenType === 'update' && existedData !== null) {
setContainerName(existedData.name)
setContainerColor(existedData.color)
setContainerIcon(existedData.icon)
} else {
setContainerName('')
setContainerColor('#FFFFFF')
setContainerIcon('tabler:cube')
}
}, [innerOpenType, existedData])
return (
<>
<Modal isOpen={openType !== null}>
<div className="mb-8 flex items-center justify-between ">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-7 w-7"
/>
{
{
create: 'Create ',
update: 'Update '
}[innerOpenType!]
}{' '}
container
</h1>
<button
onClick={() => {
setOpenType(null)
}}
className="rounded-md p-2 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
<Input
name="Container name"
icon="tabler:cube"
value={containerName}
updateValue={updateContainerName}
darker
placeholder="My container"
/>
<div className="group relative mt-6 flex items-center gap-1 rounded-t-lg border-b-2 border-neutral-500 bg-neutral-200/50 focus-within:border-custom-500 dark:bg-neutral-800/50">
<Icon
icon="tabler:palette"
className="ml-6 h-6 w-6 shrink-0 text-neutral-500 group-focus-within:text-custom-500"
/>
<div className="flex w-full items-center gap-2">
<span
className={`pointer-events-none absolute left-[4.2rem] font-medium tracking-wide text-neutral-500 group-focus-within:text-custom-500 ${
containerColor.length === 0
? 'top-1/2 -translate-y-1/2 group-focus-within:top-6 group-focus-within:text-[14px]'
: 'top-6 -translate-y-1/2 text-[14px]'
}`}
>
Container color
</span>
<div className="mr-12 mt-6 flex w-full items-center gap-2 pl-4">
<div
className="mt-0.5 h-3 w-3 shrink-0 rounded-full"
style={{
backgroundColor: containerColor
}}
></div>
<input
value={containerColor}
onChange={updateContainerColor}
placeholder="#FFFFFF"
className="h-8 w-full rounded-lg bg-transparent p-6 pl-0 tracking-wide placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-400"
/>
</div>
<button
onClick={() => {
setColorPickerOpen(true)
}}
className="mr-4 shrink-0 rounded-lg p-2 text-neutral-500 hover:bg-neutral-500/30 hover:text-neutral-200 focus:outline-none"
>
<Icon icon="tabler:color-picker" className="h-6 w-6" />
</button>
</div>
</div>
<IconInput
name="Container icon"
icon={containerIcon}
setIcon={setContainerIcon}
/>
<button
disabled={loading}
className="mt-8 flex h-16 items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-800 transition-all hover:bg-custom-600"
onClick={onSubmitButtonClick}
>
{!loading ? (
<>
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-5 w-5"
/>
{
{
create: 'CREATE',
update: 'UPDATE'
}[innerOpenType!]
}
</>
) : (
<span className="small-loader-dark"></span>
)}
</button>
</Modal>
<ColorPickerModal
isOpen={colorPickerOpen}
setOpen={setColorPickerOpen}
color={containerColor}
setColor={setContainerColor}
/>
</>
)
}
export default ModifyContainerModal

View File

@@ -1,101 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React, { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { type IIdeaBoxContainer } from '../../..'
import { toast } from 'react-toastify'
import GoBackButton from '../../../../../components/general/GoBackButton'
function ContainerHeader({ id }: { id: string }): React.ReactElement {
const [containerDetails, setContainerDetails] = useState<
IIdeaBoxContainer | 'loading' | 'error'
>('loading')
const navigate = useNavigate()
function fetchContainerDetails(): void {
setContainerDetails('loading')
fetch(`${import.meta.env.VITE_API_HOST}/idea-box/container/get/${id}`)
.then(async response => {
const data = await response.json()
setContainerDetails(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setContainerDetails('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
fetchContainerDetails()
}, [])
return (
<header className="flex flex-col gap-1 px-8 sm:px-12">
<GoBackButton
onClick={() => {
navigate('/idea-box')
}}
/>
<div className="flex items-center justify-between">
<h1
className={`flex items-center gap-4 ${
typeof containerDetails !== 'string'
? 'text-2xl sm:text-3xl'
: 'text-2xl'
} font-semibold `}
>
{(() => {
switch (containerDetails) {
case 'loading':
return (
<>
<span className="small-loader-light"></span>
Loading...
</>
)
case 'error':
return (
<>
<Icon
icon="tabler:alert-triangle"
className="mt-0.5 h-7 w-7 text-red-500"
/>
Failed to fetch data from server.
</>
)
default:
return (
<>
<div
className="rounded-lg p-3"
style={{
backgroundColor: containerDetails.color + '20'
}}
>
<Icon
icon={containerDetails.icon}
className="text-2xl sm:text-3xl"
style={{
color: containerDetails.color
}}
/>
</div>
{containerDetails.name}
</>
)
}
})()}
</h1>
<button className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:hover:bg-neutral-800 dark:hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="text-2xl" />
</button>
</div>
</header>
)
}
export default ContainerHeader

View File

@@ -1,13 +0,0 @@
import React from 'react'
function CustomZoomContent({
img
}: {
buttonUnzoom: React.ReactElement
modalState: 'LOADING' | 'LOADED' | 'UNLOADING' | 'UNLOADED'
img: any
}): React.ReactElement {
return <>{img}</>
}
export default CustomZoomContent

View File

@@ -1,140 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { type IIdeaBoxEntry } from '..'
import { toast } from 'react-toastify'
import MenuItem from '../../../../../components/general/HamburgerMenu/MenuItem'
function EntryContextMenu({
entry,
setTypeOfModifyIdea,
setModifyIdeaModalOpenType,
setExistedData,
setDeleteIdeaModalOpen,
updateIdeaList
}: {
entry: IIdeaBoxEntry
setTypeOfModifyIdea: React.Dispatch<
React.SetStateAction<'link' | 'image' | 'text'>
>
setModifyIdeaModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteIdeaModalOpen: (state: boolean) => void
updateIdeaList: () => void
}): React.ReactElement {
function pinIdea(ideaId: string): void {
fetch(`${import.meta.env.VITE_API_HOST}/idea-box/idea/pin/${ideaId}`, {
method: 'POST'
})
.then(async response => {
const data = await response.json()
if (response.status !== 200) {
throw data.message
}
toast.info("Idea's position has been updated.")
updateIdeaList()
})
.catch(() => {
toast.error('Failed to fetch data from server.')
})
}
function archiveIdea(ideaId: string): void {
fetch(`${import.meta.env.VITE_API_HOST}/idea-box/idea/archive/${ideaId}`, {
method: 'POST'
})
.then(async response => {
const data = await response.json()
if (response.status !== 200) {
throw data.message
}
toast.info('Idea has been archived.')
updateIdeaList()
})
.catch(() => {
toast.error('Failed to fetch data from server.')
})
}
return (
<Menu
as="div"
className={`${
entry.type === 'image' ? 'absolute right-2 top-2' : 'relative'
} z-[999]`}
>
<Menu.Button>
{({ open }) => (
<div
className={`shrink-0 rounded-lg bg-neutral-50 p-2 text-neutral-500 opacity-0 hover:bg-neutral-100 hover:text-neutral-800 group-hover:opacity-100 dark:bg-neutral-800/50 dark:text-neutral-100 dark:hover:bg-neutral-900 dark:hover:text-neutral-100 ${
entry.type === 'image' &&
'!shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)]'
} ${
open &&
`${
entry.type === 'image'
? '!bg-neutral-200 dark:!bg-neutral-900'
: '!bg-neutral-200 dark:!bg-neutral-800'
} !opacity-100`
}`}
>
<Icon icon="tabler:dots-vertical" className="text-xl" />
</div>
)}
</Menu.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
className="absolute right-0 top-3"
>
<Menu.Items className="mt-8 w-48 overflow-hidden rounded-md bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
<MenuItem
onClick={() => {
pinIdea(entry.id)
}}
icon={entry.pinned ? 'tabler:pinned-off' : 'tabler:pin'}
text={`${entry.pinned ? 'Unpin from' : 'Pin to'} top`}
/>
<MenuItem
onClick={() => {
archiveIdea(entry.id)
}}
icon={entry.archived ? 'tabler:archive-off' : 'tabler:archive'}
text={`${entry.archived ? 'Unarchive' : 'Archive'} idea`}
/>
{entry.type !== 'image' && (
<MenuItem
onClick={() => {
setTypeOfModifyIdea(entry.type)
setExistedData(entry)
setModifyIdeaModalOpenType('update')
}}
icon="tabler:pencil"
text="Edit"
/>
)}
<MenuItem
onClick={() => {
setExistedData(entry)
setDeleteIdeaModalOpen(true)
}}
icon="tabler:trash"
text="Delete"
isRed
/>
</Menu.Items>
</Transition>
</Menu>
)
}
export default EntryContextMenu

View File

@@ -1,100 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
function FAB({
setTypeOfModifyIdea,
setModifyIdeaModalOpenType
}: {
setTypeOfModifyIdea: React.Dispatch<
React.SetStateAction<'link' | 'image' | 'text'>
>
setModifyIdeaModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
}): React.ReactElement {
return (
<>
<Menu
as="div"
className="group fixed bottom-6 right-6 z-[9998] overscroll-contain sm:bottom-12 sm:right-12 "
>
{({ open }) => (
<>
<Menu.Button className="relative z-10 flex items-center gap-2 rounded-lg bg-custom-500 p-4 font-semibold uppercase tracking-wider text-neutral-100 shadow-lg hover:bg-custom-600 dark:text-neutral-800">
<Icon
icon="tabler:plus"
className={`h-6 w-6 shrink-0 transition-all ${
open && 'rotate-45'
}`}
/>
</Menu.Button>
<Transition
enter="transition-all ease-out duration-300 overflow-hidden"
enterFrom="max-h-0"
enterTo="max-h-96"
leave="transition-all ease-in duration-200 overflow-hidden"
leaveFrom="max-h-96"
leaveTo="max-h-0"
className="absolute bottom-0 right-0 z-10 -translate-y-16 overflow-hidden"
>
<Menu.Items className="mt-2 rounded-lg shadow-lg outline-none focus:outline-none">
<div className="py-1">
{[
['Text', 'tabler:text-size'],
['Link', 'tabler:link'],
['Image', 'tabler:photo']
].map(([name, icon]) => (
<Menu.Item key={name}>
{({ active }) => (
<button
onClick={() => {
setTypeOfModifyIdea(
name.toLowerCase() as 'text' | 'image' | 'link'
)
setModifyIdeaModalOpenType('create')
}}
className={`group flex w-full items-center justify-end gap-4 rounded-md py-3 pr-2 ${
active ? 'text-neutral-200' : 'text-neutral-100'
}`}
>
{name}
<button
className={`rounded-full ${
active ? 'bg-neutral-300' : 'bg-neutral-200'
} p-3`}
>
<Icon
icon={icon}
className={`h-5 w-5 text-neutral-800 ${
active && 'text-neutral-300'
}`}
/>
</button>
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
<div
className={`fixed left-0 top-0 h-full w-full transition-transform ${
open ? 'translate-x-0 duration-0' : 'translate-x-full delay-100'
}`}
>
<div
className={`h-full w-full bg-neutral-900/50 backdrop-blur-sm transition-opacity ${
open ? 'opacity-100' : 'opacity-0'
}`}
/>
</div>
</>
)}
</Menu>
</>
)
}
export default FAB

View File

@@ -1,57 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import { type IIdeaBoxEntry } from '../..'
import CustomZoomContent from '../CustomZoomContent'
import EntryContextMenu from '../EntryContextMenu'
import Zoom from 'react-medium-image-zoom'
import { Icon } from '@iconify/react/dist/iconify.js'
function EntryImage({
entry,
setTypeOfModifyIdea,
setModifyIdeaModalOpenType,
setExistedData,
setDeleteIdeaModalOpen,
updateIdeaList
}: {
entry: IIdeaBoxEntry
setTypeOfModifyIdea: React.Dispatch<
React.SetStateAction<'link' | 'image' | 'text'>
>
setModifyIdeaModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteIdeaModalOpen: (state: boolean) => void
updateIdeaList: () => void
}): React.ReactElement {
return (
<div className="group relative">
{entry.pinned && (
<Icon
icon="tabler:pin"
className="absolute -left-2 -top-2 z-[50] h-5 w-5 -rotate-90 text-red-500 drop-shadow-md"
/>
)}
<Zoom zoomMargin={40} ZoomContent={CustomZoomContent}>
<img
src={`${import.meta.env.VITE_POCKETBASE_ENDPOINT}/api/files/${
entry.collectionId
}/${entry.id}/${entry.image}`}
alt={''}
className="my-4 rounded-lg shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)]"
/>
</Zoom>
<EntryContextMenu
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={setModifyIdeaModalOpenType}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
</div>
)
}
export default EntryImage

View File

@@ -1,57 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import EntryContextMenu from '../EntryContextMenu'
import { Icon } from '@iconify/react/dist/iconify.js'
import { type IIdeaBoxEntry } from '../..'
function EntryLink({
entry,
setTypeOfModifyIdea,
setModifyIdeaModalOpenType,
setExistedData,
setDeleteIdeaModalOpen,
updateIdeaList
}: {
entry: IIdeaBoxEntry
setTypeOfModifyIdea: React.Dispatch<
React.SetStateAction<'link' | 'image' | 'text'>
>
setModifyIdeaModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteIdeaModalOpen: (state: boolean) => void
updateIdeaList: () => void
}): React.ReactElement {
return (
<div className="relative my-4 flex flex-col gap-2 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
{entry.pinned && (
<Icon
icon="tabler:pin"
className="absolute -left-2 -top-2 z-[50] h-5 w-5 -rotate-90 text-red-500 drop-shadow-md"
/>
)}
<div className="flex items-start justify-between gap-2">
<h3 className="text-xl font-semibold ">{entry.title}</h3>
<EntryContextMenu
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={setModifyIdeaModalOpenType}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
</div>
<a
target="_blank"
rel="noreferrer"
href={entry.content}
className="text-custom-500 underline underline-offset-2"
>
{entry.content}
</a>
</div>
)
}
export default EntryLink

View File

@@ -1,49 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import EntryContextMenu from '../EntryContextMenu'
import { type IIdeaBoxEntry } from '../..'
function EntryText({
entry,
setTypeOfModifyIdea,
setModifyIdeaModalOpenType,
setExistedData,
setDeleteIdeaModalOpen,
updateIdeaList
}: {
entry: IIdeaBoxEntry
setTypeOfModifyIdea: React.Dispatch<
React.SetStateAction<'link' | 'image' | 'text'>
>
setModifyIdeaModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteIdeaModalOpen: (state: boolean) => void
updateIdeaList: () => void
}): React.ReactElement {
return (
<div className="group relative my-4 flex items-start justify-between gap-2 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
{entry.pinned && (
<Icon
icon="tabler:pin"
className="absolute -left-2 -top-2 z-[50] h-5 w-5 -rotate-90 text-red-500 drop-shadow-md"
/>
)}
<p className="mt-1.5 text-neutral-800 dark:text-neutral-100">
{entry.content}
</p>
<EntryContextMenu
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={setModifyIdeaModalOpenType}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
</div>
)
}
export default EntryText

View File

@@ -1,191 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/indent */
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
// @ts-expect-error - no types available
import Column from 'react-columns'
import { toast } from 'react-toastify'
import Loading from '../../../../components/general/Loading'
import Error from '../../../../components/general/Error'
import EmptyStateScreen from '../../../../components/general/EmptyStateScreen'
import CreateIdeaModal from './modals/ModifyIdeaModal'
import DeleteIdeaConfirmationModal from './modals/DeleteIdeaConfirmationModal'
import EntryImage from './components/IdeaEntry/EntryImage'
import EntryText from './components/IdeaEntry/EntryText'
import EntryLink from './components/IdeaEntry/EntryLink'
import ContainerHeader from './components/ContainerHeader'
import FAB from './components/FAB'
export interface IIdeaBoxEntry {
collectionId: string
collectionName: string
container: string
content: string
created: string
id: string
image: string
title: string
type: 'text' | 'image' | 'link'
updated: string
pinned: boolean
archived: boolean
}
function Ideas(): React.JSX.Element {
const { id } = useParams<{ id: string }>()
const [data, setData] = useState<IIdeaBoxEntry[] | 'error' | 'loading'>(
'loading'
)
const [modifyIdeaModalOpenType, setModifyIdeaModalOpenType] = useState<
null | 'create' | 'update'
>(null)
const [typeOfModifyIdea, setTypeOfModifyIdea] = useState<
'text' | 'image' | 'link'
>('text')
const [existedData, setExistedData] = useState<IIdeaBoxEntry | null>(null)
const [deleteIdeaModalOpen, setDeleteIdeaModalOpen] = useState(false)
function updateIdeaList(): void {
setData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/idea-box/idea/list/${id}`)
.then(async response => {
const data = await response.json()
setData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setData('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateIdeaList()
}, [])
return (
<>
<section className="relative min-h-0 w-full min-w-0 flex-1 overflow-y-auto">
<ContainerHeader id={id!} />
{(() => {
switch (data) {
case 'loading':
return <Loading />
case 'error':
return <Error message="Failed to fetch data from server." />
default:
return data.length > 0 ? (
<Column
queries={[
{
columns: 1,
query: 'min-width: 0px'
},
{
columns: 2,
query: 'min-width: 768px'
},
{
columns: 3,
query: 'min-width: 1024px'
},
{
columns: 4,
query: 'min-width: 1280px'
},
{
columns: 5,
query: 'min-width: 1536px'
}
]}
gap="0.5rem"
className="mt-8 h-max px-8 sm:px-12"
>
{data.map(entry => {
switch (entry.type) {
case 'image':
return (
<EntryImage
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={
setModifyIdeaModalOpenType
}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
)
case 'text':
return (
<EntryText
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={
setModifyIdeaModalOpenType
}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
)
case 'link':
return (
<EntryLink
entry={entry}
setTypeOfModifyIdea={setTypeOfModifyIdea}
setModifyIdeaModalOpenType={
setModifyIdeaModalOpenType
}
setExistedData={setExistedData}
setDeleteIdeaModalOpen={setDeleteIdeaModalOpen}
updateIdeaList={updateIdeaList}
/>
)
}
return <></>
})}
</Column>
) : (
<EmptyStateScreen
setModifyModalOpenType={setModifyIdeaModalOpenType}
title="No ideas yet"
description="Hmm... Seems a bit empty here. Consider adding some innovative ideas."
icon="tabler:bulb-off"
ctaContent="new idea"
/>
)
}
})()}
<FAB
setModifyIdeaModalOpenType={setModifyIdeaModalOpenType}
setTypeOfModifyIdea={setTypeOfModifyIdea}
/>
</section>
<CreateIdeaModal
openType={modifyIdeaModalOpenType}
typeOfModifyIdea={typeOfModifyIdea}
setOpenType={setModifyIdeaModalOpenType}
containerId={id as string}
updateIdeaList={updateIdeaList}
existedData={existedData}
/>
<DeleteIdeaConfirmationModal
isOpen={deleteIdeaModalOpen}
closeModal={() => {
setDeleteIdeaModalOpen(false)
}}
ideaDetails={existedData}
setData={setData}
/>
</>
)
}
export default Ideas

View File

@@ -1,93 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable multiline-ternary */
import React, { useState } from 'react'
import Modal from '../../../../../components/general/Modal'
import { toast } from 'react-toastify'
import { Icon } from '@iconify/react/dist/iconify.js'
import { type IIdeaBoxEntry } from '..'
function DeleteIdeaConfirmationModal({
isOpen,
setData,
closeModal,
ideaDetails
}: {
isOpen: boolean
setData: React.Dispatch<
React.SetStateAction<IIdeaBoxEntry[] | 'error' | 'loading'>
>
closeModal: () => void
ideaDetails: IIdeaBoxEntry | null
}): React.ReactElement {
const [loading, setLoading] = useState(false)
function deleteIdea(): void {
if (ideaDetails === null) return
setLoading(true)
fetch(
`${import.meta.env.VITE_API_HOST}/idea-box/idea/delete/${ideaDetails.id}`,
{
method: 'DELETE'
}
)
.then(async res => {
const data = await res.json()
if (res.ok) {
toast.info("Uhh, hopefully you truly didn't need that idea.")
closeModal()
setData(prev => {
if (prev === 'error' || prev === 'loading') return prev
return prev.filter(idea => idea.id !== ideaDetails?.id)
})
return data
} else {
throw new Error(data.message)
}
})
.catch(err => {
toast.error("Oops! Couldn't delete the idea. Please try again.")
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
return (
<Modal isOpen={isOpen}>
<h1 className="text-2xl font-semibold">
Are you sure you want to delete this {ideaDetails?.type} idea?
</h1>
<p className="mt-2 text-neutral-500">
This idea will be gone forever. This action is irreversible!
</p>
<div className="mt-8 flex w-full justify-around gap-2">
<button
onClick={closeModal}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-800 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-neutral-700"
>
Cancel
</button>
<button
disabled={loading}
onClick={deleteIdea}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-red-600"
>
{loading ? (
<>
<span className="small-loader-light"></span>
</>
) : (
<>
<Icon icon="tabler:trash" className="h-5 w-5" />
DELETE
</>
)}
</button>
</div>
</Modal>
)
}
export default DeleteIdeaConfirmationModal

View File

@@ -1,444 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useEffect, useState, useCallback, useContext } from 'react'
import Modal from '../../../../../components/general/Modal'
import { Icon } from '@iconify/react/dist/iconify.js'
import { useDebounce } from '@uidotdev/usehooks'
import { Menu, Transition } from '@headlessui/react'
import { useDropzone } from 'react-dropzone'
import { toast } from 'react-toastify'
import { type IIdeaBoxEntry } from '..'
import { PersonalizationContext } from '../../../../../providers/PersonalizationProvider'
function CreateIdeaModal({
openType,
setOpenType,
typeOfModifyIdea,
containerId,
updateIdeaList,
existedData
}: {
openType: 'create' | 'update' | null
setOpenType: React.Dispatch<React.SetStateAction<'create' | 'update' | null>>
typeOfModifyIdea: 'text' | 'image' | 'link'
containerId: string
updateIdeaList: () => void
existedData: IIdeaBoxEntry | null
}): React.ReactElement {
const { theme } = useContext(PersonalizationContext)
const innerOpenType = useDebounce(openType, openType === null ? 300 : 0)
const [innerTypeOfModifyIdea, setInnerTypeOfModifyIdea] = useState<
'text' | 'image' | 'link'
>('text')
const [ideaTitle, setIdeaTitle] = useState('')
const [ideaContent, setIdeaContent] = useState('')
const [ideaLink, setIdeaLink] = useState('')
const [ideaImage, setIdeaImage] = useState<File | null>(null)
const [preview, setPreview] = useState<string | ArrayBuffer | null>(null)
const [loading, setLoading] = useState(false)
const onDrop = useCallback((acceptedFiles: File[]) => {
const file = new FileReader()
file.onload = function () {
setPreview(file.result)
}
file.readAsDataURL(acceptedFiles[0])
setIdeaImage(acceptedFiles[0])
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop
})
function updateIdeaTitle(event: React.ChangeEvent<HTMLInputElement>): void {
setIdeaTitle(event.target.value)
}
function updateIdeaContent(
event: React.FormEvent<HTMLTextAreaElement>
): void {
setIdeaContent(event.currentTarget.value)
}
function updateIdeaLink(event: React.ChangeEvent<HTMLInputElement>): void {
setIdeaLink(event.target.value)
}
useEffect(() => {
setInnerTypeOfModifyIdea(typeOfModifyIdea)
}, [typeOfModifyIdea])
useEffect(() => {
if (openType === 'create') {
setIdeaTitle('')
setIdeaContent('')
setIdeaLink('')
setIdeaImage(null)
setPreview(null)
} else if (openType === 'update') {
if (existedData !== null) {
setIdeaTitle(existedData.title)
setIdeaContent(existedData.content)
setIdeaLink(existedData.content)
setIdeaImage(null)
setPreview(null)
}
}
}, [openType, existedData])
function onSubmitButtonClick(): void {
switch (innerTypeOfModifyIdea) {
case 'text':
if (ideaContent.trim().length === 0) {
toast.error('Idea content cannot be empty.')
return
}
break
case 'image':
if (ideaImage === null) {
toast.error('Idea image cannot be empty.')
return
}
break
case 'link':
if (ideaTitle.trim().length === 0 || ideaLink.trim().length === 0) {
toast.error('Idea title and link cannot be empty.')
return
}
break
}
setLoading(true)
const formData = new FormData()
formData.append('title', ideaTitle.trim())
formData.append('content', ideaContent.trim())
formData.append('link', ideaLink.trim())
formData.append('image', ideaImage!)
formData.append('type', innerTypeOfModifyIdea)
fetch(
`${import.meta.env.VITE_API_HOST}/idea-box/idea/${
innerOpenType === 'create' ? 'create' : 'update'
}/${innerOpenType === 'create' ? containerId : existedData!.id}`,
{
method: innerOpenType === 'create' ? 'PUT' : 'PATCH',
...(innerOpenType === 'update' && {
headers: {
'Content-Type': 'application/json'
}
}),
body:
innerOpenType === 'create'
? formData
: JSON.stringify({
title: ideaTitle.trim(),
content: ideaContent.trim(),
link: ideaLink.trim(),
type: innerTypeOfModifyIdea
})
}
)
.then(async res => {
const data = await res.json()
if (res.ok) {
toast.success(
`Yay! Idea ${
innerOpenType === 'create' ? 'created' : 'updated'
} successfully.`
)
updateIdeaList()
setOpenType(null)
return data
} else {
throw new Error(data.message)
}
})
.catch(err => {
toast.error(
`Oops! Couldn't ${
innerOpenType === 'create' ? 'create' : 'update'
} the idea. Please try again.`
)
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
return (
<Modal isOpen={openType !== null}>
<div className="mb-8 flex w-[50vw] items-center justify-between">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-7 w-7"
/>
{
{
create: 'New ',
update: 'Update '
}[innerOpenType!]
}{' '}
{innerOpenType === 'create' ? (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="inline-flex w-full items-center justify-center rounded-md border-2 border-neutral-300 p-2 px-4 text-lg font-semibold tracking-wide text-neutral-800 shadow-sm outline-none hover:bg-neutral-200/50 focus:outline-none dark:border-neutral-800 dark:bg-neutral-800/50 dark:text-neutral-200">
<Icon
icon={
{
text: 'tabler:article',
image: 'tabler:photo',
link: 'tabler:link'
}[innerTypeOfModifyIdea]
}
className="mr-2 h-5 w-5"
/>
{innerTypeOfModifyIdea === 'text'
? 'Text'
: innerTypeOfModifyIdea === 'image'
? 'Image'
: 'Link'}
<Icon
icon="tabler:chevron-down"
className="-mr-1 ml-2 h-4 w-4 stroke-[2px]"
aria-hidden="true"
/>
</Menu.Button>
<Transition
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
className="absolute left-0 z-[999] mt-2"
>
<Menu.Items className="w-56 overflow-hidden rounded-lg bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
{[
['text', 'tabler:article', 'Text'],
...[['image', 'tabler:photo', 'Image']],
['link', 'tabler:link', 'Link']
].map(([type, icon, name]) => (
<Menu.Item key={type}>
{({ active }) => (
<button
onClick={() => {
setInnerTypeOfModifyIdea(
type as 'text' | 'image' | 'link'
)
}}
className={`group flex w-full items-center rounded-md p-4 text-base ${
type === innerTypeOfModifyIdea
? 'text-neutral-800 dark:text-neutral-100'
: active
? 'bg-neutral-200/50 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100'
: 'text-neutral-500 hover:bg-neutral-200/50 dark:text-neutral-400 dark:hover:bg-neutral-800'
}`}
>
<Icon
icon={icon}
className="mr-3 h-5 w-5"
aria-hidden="true"
/>
{name}
{innerTypeOfModifyIdea === type && (
<Icon
icon="tabler:check"
className="ml-auto h-5 w-5"
aria-hidden="true"
/>
)}
</button>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
) : (
innerTypeOfModifyIdea[0].toUpperCase() +
innerTypeOfModifyIdea.slice(1) +
' '
)}
Idea
</h1>
<button
onClick={() => {
setOpenType(null)
}}
className="rounded-md p-2 text-neutral-500 transition-all hover:bg-neutral-200/50 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
{innerTypeOfModifyIdea === 'link' && (
<div className="group relative mb-6 flex items-center gap-1 rounded-t-lg border-b-2 border-neutral-500 bg-neutral-50 focus-within:border-custom-500 dark:bg-neutral-800/50">
<Icon
icon="tabler:bulb"
className="ml-6 h-6 w-6 shrink-0 text-neutral-500 group-focus-within:text-custom-500"
/>
<div className="flex w-full items-center gap-2">
<span
className={`pointer-events-none absolute left-[4.2rem] font-medium tracking-wide text-neutral-500 group-focus-within:text-custom-500 ${
ideaTitle.length === 0
? 'top-1/2 -translate-y-1/2 group-focus-within:top-6 group-focus-within:text-[14px]'
: 'top-6 -translate-y-1/2 text-[14px]'
}`}
>
Idea title
</span>
<input
value={ideaTitle}
onChange={updateIdeaTitle}
placeholder="Mind blowing idea"
className="mt-6 h-8 w-full rounded-lg bg-transparent p-6 pl-4 tracking-wide placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-400"
/>
</div>
</div>
)}
{innerTypeOfModifyIdea !== 'image' ? (
<div
onFocus={e => {
e.currentTarget.querySelector('textarea input')?.focus()
}}
className="group relative flex items-center gap-1 rounded-t-lg border-b-2 border-neutral-500 bg-neutral-50 focus-within:border-custom-500 dark:bg-neutral-800/50"
>
<Icon
icon={
innerTypeOfModifyIdea === 'text'
? 'tabler:file-text'
: 'tabler:link'
}
className="ml-6 h-6 w-6 shrink-0 text-neutral-500 group-focus-within:text-custom-500"
/>
<div className="flex w-full items-center gap-2">
<span
className={`pointer-events-none absolute left-[4.2rem] font-medium tracking-wide text-neutral-500 group-focus-within:text-custom-500 ${
{
text: ideaContent,
link: ideaLink
}[innerTypeOfModifyIdea].length === 0
? 'top-1/2 -translate-y-1/2 group-focus-within:top-6 group-focus-within:text-[14px]'
: 'top-6 -translate-y-1/2 text-[14px]'
}`}
>
{innerTypeOfModifyIdea === 'text'
? 'Idea content'
: 'Link to idea'}
</span>
{innerTypeOfModifyIdea === 'text' ? (
<textarea
value={ideaContent}
onInput={e => {
e.currentTarget.style.height = 'auto'
e.currentTarget.style.height =
e.currentTarget.scrollHeight + 'px'
updateIdeaContent(e)
}}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, lorem euismod."
className="mt-6 min-h-[2rem] w-full resize-none rounded-lg bg-transparent p-6 pl-4 tracking-wide outline-none placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-400"
/>
) : (
<input
value={ideaLink}
onChange={updateIdeaLink}
placeholder="https://example.com"
className="mt-6 h-8 w-full rounded-lg bg-transparent p-6 pl-4 tracking-wide placeholder:text-transparent focus:outline-none focus:placeholder:text-neutral-400"
/>
)}
</div>
</div>
) : preview ? (
<div className="flex items-center justify-center">
<div className="relative flex h-[30rem] min-h-[8rem] w-full items-center justify-center overflow-hidden rounded-lg bg-neutral-800">
<img
src={preview as string}
alt="preview"
className="h-full w-full object-scale-down"
/>
<button
onClick={() => {
setPreview(null)
}}
className="absolute right-4 top-4 rounded-lg bg-neutral-800 p-2 text-neutral-500 transition-all hover:bg-neutral-900"
>
<Icon icon="tabler:x" className="h-5 w-5" />
</button>
</div>
</div>
) : (
<div
className="flex w-full flex-col items-center justify-center rounded-lg border-[3px] border-dashed border-neutral-500 py-12"
{...getRootProps()}
>
<input {...getInputProps()} />
<Icon
icon="tabler:drag-drop"
className="h-20 w-20 text-neutral-500"
/>
<div className="mt-4 text-center text-2xl font-medium text-neutral-500">
{isDragActive ? "Drop it like it's hot" : 'Drag and drop to upload'}
</div>
<div className="mt-4 text-center text-lg font-semibold uppercase tracking-widest text-neutral-400">
or
</div>
<label
htmlFor="idea-image"
className="mt-4 flex items-center gap-2 rounded-lg bg-neutral-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-neutral-600 dark:bg-neutral-100 dark:text-neutral-800 dark:hover:bg-neutral-200"
>
<Icon icon="tabler:upload" className="h-5 w-5" />
Upload image
</label>
</div>
)}
<button
disabled={loading}
onClick={onSubmitButtonClick}
className="mt-8 flex h-16 items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-custom-600 dark:text-neutral-800"
>
{!loading ? (
<>
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-5 w-5"
/>
{
{
create: 'CREATE',
update: 'UPDATE'
}[innerOpenType!]
}
</>
) : (
<span
className={
(theme === 'system' &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) ||
theme === 'dark'
? 'small-loader-dark'
: 'small-loader-light'
}
></span>
)}
</button>
</Modal>
)
}
export default CreateIdeaModal

View File

@@ -1,151 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { Icon } from '@iconify/react'
import { toast } from 'react-toastify'
import Loading from '../../components/general/Loading'
import Error from '../../components/general/Error'
import ModifyContainerModal from './components/Container/modals/ModifyContainerModal'
import { useDebounce } from '@uidotdev/usehooks'
import EmptyStateScreen from '../../components/general/EmptyStateScreen'
import Container from './components/Container'
import DeleteContainerConfirmationModal from './components/Container/modals/DeleteContainerConfirmationModal'
export interface IIdeaBoxContainer {
collectionId: string
collectionName: string
color: string
created: string
icon: string
id: string
image_count: number
link_count: number
name: string
text_count: number
updated: string
}
function IdeaBox(): React.JSX.Element {
const [data, setData] = useState<IIdeaBoxContainer[] | 'error' | 'loading'>(
'loading'
)
const [modifyContainerModalOpenType, setModifyContainerModalOpenType] =
useState<'create' | 'update' | null>(null)
const [
deleteContainerConfirmationModalOpen,
setDeleteContainerConfirmationModalOpen
] = useState(false)
const [existedData, setExistedData] = useState<IIdeaBoxContainer | null>(null)
const [filteredList, setFilteredList] = useState<IIdeaBoxContainer[]>([])
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearchQuery = useDebounce(searchQuery, 300)
function updateContainerList(): void {
setData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/idea-box/container/list`)
.then(async response => {
const data = await response.json()
setData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setData('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateContainerList()
}, [])
useEffect(() => {
if (Array.isArray(data)) {
if (debouncedSearchQuery.length === 0) {
setFilteredList(data)
} else {
setFilteredList(
data.filter(container =>
container.name
.toLowerCase()
.includes(debouncedSearchQuery.toLowerCase())
)
)
}
}
}, [debouncedSearchQuery, data])
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-8 sm:px-12">
<ModuleHeader
title="Idea Box"
desc="Sometimes you will randomly stumble upon a great idea."
/>
<div className="mt-8 flex min-h-0 w-full flex-1 flex-col">
<search className="flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value)
}}
placeholder="Search idea containers ..."
className="w-full bg-transparent text-neutral-500 placeholder:text-neutral-400 focus:outline-none"
/>
</search>
<>
{(() => {
switch (data) {
case 'loading':
return <Loading />
case 'error':
return <Error message="Failed to fetch data from server." />
default:
return data.length > 0 ? (
<Container
filteredList={filteredList}
setCreateContainerModalOpen={
setModifyContainerModalOpenType
}
setExistedData={setExistedData}
setDeleteContainerConfirmationModalOpen={
setDeleteContainerConfirmationModalOpen
}
/>
) : (
<EmptyStateScreen
setModifyModalOpenType={setModifyContainerModalOpenType}
title="No idea containers"
description="Hmm... Seems a bit empty here. Consider creating one."
icon="tabler:cube-off"
ctaContent="Create container"
/>
)
}
})()}
</>
</div>
<ModifyContainerModal
openType={modifyContainerModalOpenType}
setOpenType={setModifyContainerModalOpenType}
updateContainerList={updateContainerList}
existedData={existedData}
/>
<DeleteContainerConfirmationModal
isOpen={deleteContainerConfirmationModalOpen}
closeModal={() => {
setExistedData(null)
setDeleteContainerConfirmationModalOpen(false)
}}
containerDetails={existedData}
updateContainerList={updateContainerList}
/>
</section>
)
}
export default IdeaBox

View File

@@ -1,387 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/indent */
import { Menu, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
import React, { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import GoBackButton from '../../../../../../components/general/GoBackButton'
import MenuItem from '../../../../../../components/general/HamburgerMenu/MenuItem'
import { type INotesEntry, type INotesPath } from '../../..'
import { toast } from 'react-toastify'
function DirectoryHeader({
updateNotesEntries,
setModifyFolderModalOpenType,
setExistedData
}: {
updateNotesEntries: () => void
setModifyFolderModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: React.Dispatch<React.SetStateAction<INotesEntry | null>>
}): React.ReactElement {
const [currentPath, setCurrentPath] = useState<
| {
icon: string
path: INotesPath[]
}
| 'loading'
| 'error'
>('loading')
const {
workspace,
subject,
'*': path
} = useParams<{
workspace: string
subject: string
'*': string
}>()
const toastId = React.useRef<any>()
const navigate = useNavigate()
function fetchCurrentPath(): void {
setCurrentPath('loading')
fetch(
`${
import.meta.env.VITE_API_HOST
}/notes/entry/path/${workspace}/${subject}/${path}`
)
.then(async response => {
const data = await response.json()
if (response.status !== 200) {
throw data.message
}
setCurrentPath(data.data)
})
.catch(() => {
setCurrentPath('error')
toast.error('Failed to fetch data from server.')
})
}
function uploadFiles(): void {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.multiple = true
fileInput.click()
fileInput.addEventListener('change', () => {
const files = fileInput.files
if (files === null) {
return
}
const formData = new FormData()
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i], encodeURIComponent(files[i].name))
}
formData.append(
'parent',
path !== undefined ? path.split('/').pop()! : ''
)
fetch(
`${
import.meta.env.VITE_API_HOST
}/notes/entry/upload/${workspace}/${subject}/${path}`,
{
method: 'PUT',
body: formData
}
)
.then(async response => {
const data = await response.json()
if (response.status !== 200) {
throw data.message
}
toast.success('Yay! Files uploaded.')
updateNotesEntries()
})
.catch(err => {
toast.error('Failed to upload files. Error: ' + err)
})
})
}
function uploadFolders(): void {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.multiple = true
fileInput.directory = true
fileInput.webkitdirectory = true
fileInput.click()
fileInput.addEventListener('change', async () => {
const files = fileInput.files
let uploaded = 0
if (files === null) {
return
}
const filesChunk = []
for (let i = 0; i < files.length; i += 10) {
filesChunk.push(Array.from(files).slice(i, i + 10))
}
for (const chunk of filesChunk) {
const formData = new FormData()
for (let i = 0; i < chunk.length; i++) {
formData.append(
'files',
chunk[i],
encodeURIComponent(chunk[i].webkitRelativePath)
)
}
formData.append(
'parent',
path !== undefined ? path.split('/').pop()! : ''
)
await fetch(
`${
import.meta.env.VITE_API_HOST
}/notes/entry/upload/${workspace}/${subject}/${path}`,
{
method: 'POST',
body: formData
}
)
.then(async response => {
const data = await response.json()
if (response.status !== 200) {
throw data.message
}
})
.catch(err => {
toast.error('Failed to upload folders. Error: ' + err)
})
uploaded += chunk.length
const progress = uploaded / files.length
// check if we already displayed a toast
if (toastId.current === undefined) {
toastId.current = toast(
<span className="flex items-center gap-2">
<Icon icon="tabler:upload" className="h-5 w-5" />
<span>Uploading folders...</span>
</span>,
{ progress }
)
} else {
toast.update(toastId.current, { progress })
}
}
updateNotesEntries()
toast.done(toastId.current)
toast.dismiss(toastId.current)
toastId.current = undefined
toast.success('Yay! Folders uploaded.')
})
}
useEffect(() => {
fetchCurrentPath()
}, [path])
return (
<>
<GoBackButton
onClick={() => {
navigate(
`/notes/${(() => {
if (path === undefined || path === '') {
return subject !== undefined ? workspace : '/'
}
const pathArray = path.split('/')
pathArray.pop()
return `${workspace}/${subject}/${pathArray.join('/')}`
})()}`
)
}}
/>
<div className="relative z-[100] flex w-full items-center justify-between gap-4 sm:gap-12">
<div
className={`flex min-w-0 flex-1 items-center gap-4 ${
typeof currentPath !== 'string'
? 'text-2xl sm:text-3xl'
: 'text-2xl'
} font-semibold`}
>
{(() => {
switch (currentPath) {
case 'loading':
return (
<>
<span className="small-loader-light"></span>
Loading...
</>
)
case 'error':
return (
<>
<Icon
icon="tabler:alert-triangle"
className="mt-0.5 h-7 w-7 text-red-500"
/>
Failed to fetch data from server.
</>
)
default:
return (
<>
<div className="relative rounded-lg p-3">
<Icon
icon={currentPath.icon}
className="text-2xl text-custom-500 sm:text-3xl"
/>
<div className="absolute left-0 top-0 h-full w-full rounded-lg bg-custom-500 opacity-20" />
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
<div className="hidden items-center gap-1 text-sm text-neutral-500 md:flex">
{currentPath.path.map((path, index) => (
<>
<Link
to={`/notes/${currentPath.path
.slice(0, index + 1)
.map(path => path.id)
.join('/')}`}
key={index}
className={`${
index === currentPath.path.length - 1
? 'text-custom-500'
: ''
} whitespace-nowrap`}
>
{path.name.slice(0, 20) +
(path.name.length > 20 ? '...' : '')}
</Link>
{index !== currentPath.path.length - 1 && (
<Icon
icon="tabler:chevron-right"
className="h-4 w-4 shrink-0 text-neutral-500"
/>
)}
</>
))}
</div>
<h1 className="w-full truncate">
{currentPath.path[currentPath.path.length - 1].name}
</h1>
</div>
</>
)
}
})()}
</div>
<div className="flex items-center gap-4">
<button className="hidden rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-900 dark:hover:bg-neutral-700/50 dark:hover:text-neutral-100 md:block">
<Icon icon="tabler:search" className="text-2xl" />
</button>
<button className="hidden rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-900 dark:hover:bg-neutral-700/50 dark:hover:text-neutral-100 md:block">
<Icon icon="tabler:filter" className="text-2xl" />
</button>
<Menu as="div" className="relative z-50 hidden md:block">
<Menu.Button className="flex items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] transition-all hover:bg-custom-600 dark:text-neutral-800">
<Icon icon="tabler:plus" className="text-xl" />
new
</Menu.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
className="absolute right-0 top-8"
>
<Menu.Items className="mt-8 w-48 overflow-hidden overscroll-contain rounded-md bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
<MenuItem
onClick={() => {
setModifyFolderModalOpenType('create')
setExistedData(null)
}}
icon="tabler:folder-plus"
text="New Folder"
/>
<div className="w-full border-b border-neutral-300 dark:border-neutral-700" />
<MenuItem
onClick={uploadFiles}
icon="ci:file-upload"
text="File upload"
/>
<MenuItem
onClick={uploadFolders}
icon="ci:folder-upload"
text="Folder upload"
/>
</Menu.Items>
</Transition>
</Menu>
<button className="rounded-lg p-4 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:hover:bg-neutral-800 dark:hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="text-xl sm:text-2xl" />
</button>
</div>
</div>
<Menu as="div" className="absolute bottom-8 right-8 z-50 md:hidden">
<Menu.Button className="flex items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] transition-all hover:bg-custom-600 dark:text-neutral-800">
<Icon icon="tabler:plus" className="text-xl" />
new
</Menu.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
className="absolute right-0 top-8"
>
<Menu.Items className="mt-8 w-48 overflow-hidden overscroll-contain rounded-md bg-neutral-100 shadow-lg outline-none focus:outline-none dark:bg-neutral-800">
<MenuItem
onClick={() => {
setModifyFolderModalOpenType('create')
setExistedData(null)
}}
icon="tabler:folder-plus"
text="New Folder"
/>
<div className="w-full border-b border-neutral-300 dark:border-neutral-700" />
<MenuItem
onClick={uploadFiles}
icon="ci:file-upload"
text="File upload"
/>
<MenuItem
onClick={uploadFolders}
icon="ci:folder-upload"
text="Folder upload"
/>
</Menu.Items>
</Transition>
</Menu>
</>
)
}
export default DirectoryHeader

View File

@@ -1,24 +0,0 @@
/* eslint-disable multiline-ternary */
import React from 'react'
import { Link } from 'react-router-dom'
import { type INotesEntry } from '../../../../..'
function EntryButton({ entry }: { entry: INotesEntry }): React.ReactElement {
return entry.type === 'folder' ? (
<Link
to={`./${entry.id}`}
className="absolute left-0 top-0 h-full w-full rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800/50"
/>
) : (
<a
href={`${import.meta.env.VITE_POCKETBASE_ENDPOINT}/api/files/${
entry.collectionId
}/${entry.id}/${entry.file}`}
target="_blank"
rel="noreferrer"
className="absolute left-0 top-0 h-full w-full rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800/50"
/>
)
}
export default EntryButton

View File

@@ -1,26 +0,0 @@
import moment from 'moment'
import React from 'react'
import { Tooltip } from 'react-tooltip'
function EntryCreationDate({
id,
date
}: {
id: string
date: string
}): React.ReactElement {
return (
<div className="z-50 hidden w-1/5 shrink-0 items-center md:flex">
<div
data-tooltip-id={`date-tooltip-${id}`}
data-tooltip-content={moment(date).format('MMMM Do YYYY, h:mm:ss a')}
className="z-50 shrink-0 text-neutral-500 dark:text-neutral-400"
>
{moment(date).fromNow()}
</div>
<Tooltip id={`date-tooltip-${id}`} className="z-50" />
</div>
)
}
export default EntryCreationDate

View File

@@ -1,43 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import HamburgerMenu from '../../../../../../../../components/general/HamburgerMenu'
import MenuItem from '../../../../../../../../components/general/HamburgerMenu/MenuItem'
import { type INotesEntry } from '../../../../..'
function EntryMenu({
entry,
setModifyFolderModalOpenType,
setExistedData,
setDeleteFolderConfirmationModalOpen
}: {
entry: INotesEntry
setModifyFolderModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteFolderConfirmationModalOpen: (state: boolean) => void
}): React.ReactElement {
return (
<HamburgerMenu position="relative">
<MenuItem
icon="tabler:edit"
onClick={() => {
setModifyFolderModalOpenType('update')
setExistedData(entry)
}}
text="Rename"
/>
<MenuItem
icon="tabler:trash"
onClick={() => {
setDeleteFolderConfirmationModalOpen(true)
setExistedData(entry)
}}
text="Delete"
isRed
/>
</HamburgerMenu>
)
}
export default EntryMenu

View File

@@ -1,11 +0,0 @@
import React from 'react'
function EntryName({ name }: { name: string }): React.ReactElement {
return (
<div className="pointer-events-none z-50 w-[20rem] truncate text-lg font-medium text-neutral-900 dark:text-neutral-100">
{name}
</div>
)
}
export default EntryName

View File

@@ -1,60 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import FILE_ICONS from '../../../../../../../constants/file_icons'
import { type INotesEntry } from '../../../..'
import EntryName from './components/EntryName'
import EntryCreationDate from './components/EntryCreationDate'
import EntryButton from './components/EntryButton'
import EntryMenu from './components/EntryMenu'
function EntryItem({
entry,
setModifyFolderModalOpenType,
setExistedData,
setDeleteFolderConfirmationModalOpen
}: {
entry: INotesEntry
setModifyFolderModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteFolderConfirmationModalOpen: (state: boolean) => void
}): React.ReactElement {
return (
<li
key={entry.id}
className="relative mt-0 flex min-w-0 items-center justify-between gap-4 p-6"
>
<Icon
icon={
{
file:
FILE_ICONS[
entry.name.split('.').pop()! as keyof typeof FILE_ICONS
] ?? 'tabler:file',
folder: 'tabler:folder'
}[entry.type]
}
className="pointer-events-auto z-50 h-7 w-7 shrink-0 text-neutral-500"
/>
<div className="flex w-full min-w-0 items-center justify-between gap-8">
<EntryName name={entry.name} />
<EntryCreationDate date={entry.created} id={entry.id} />
<EntryButton entry={entry} />
<EntryMenu
entry={entry}
setModifyFolderModalOpenType={setModifyFolderModalOpenType}
setExistedData={setExistedData}
setDeleteFolderConfirmationModalOpen={
setDeleteFolderConfirmationModalOpen
}
/>
</div>
</li>
)
}
export default EntryItem

View File

@@ -1,44 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/indent */
import React from 'react'
import { type INotesEntry } from '../..'
import EntryItem from './components/EntryItem'
function Directory({
notesEntries,
setModifyFolderModalOpenType,
setExistedData,
setDeleteFolderConfirmationModalOpen
}: {
notesEntries: INotesEntry[]
setModifyFolderModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteFolderConfirmationModalOpen: (state: boolean) => void
}): React.ReactElement {
return (
<ul className="mt-6 flex h-full min-h-0 flex-col divide-y divide-neutral-300 overflow-y-auto dark:divide-neutral-700/50">
{notesEntries
.sort(
(a, b) =>
-a.type.localeCompare(b.type) || a.name.localeCompare(b.name)
)
.map(entry => (
<EntryItem
key={entry.id}
entry={entry}
setModifyFolderModalOpenType={setModifyFolderModalOpenType}
setExistedData={setExistedData}
setDeleteFolderConfirmationModalOpen={
setDeleteFolderConfirmationModalOpen
}
/>
))}
</ul>
)
}
export default Directory

View File

@@ -1,129 +0,0 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/member-delimiter-style */
/* eslint-disable @typescript-eslint/indent */
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import Error from '../../../components/general/Error'
import Loading from '../../../components/general/Loading'
import EmptyStateScreen from '../../../components/general/EmptyStateScreen'
import ModifyFolderModal from './modals/ModifyFolderModal'
import DeleteFolderConfirmationModal from './modals/DeleteFolderConfirmationModal'
import DirectoryHeader from './components/Directory/components/DirectoryHeader'
import Directory from './components/Directory'
// Generated by https://quicktype.io
export interface INotesEntry {
collectionId: string
collectionName: string
created: string
id: string
name: string
path: string
subject: string
type: 'file' | 'folder'
updated: string
file: string
}
export interface INotesPath {
id: string
name: string
}
function NotesSubject(): React.ReactElement {
const { subject, '*': path } = useParams<{ subject: string; '*': string }>()
const [notesEntries, setNotesEntries] = useState<
INotesEntry[] | 'loading' | 'error'
>('loading')
const [modifyFolderModalOpenType, setModifyFolderModalOpenType] = useState<
'create' | 'update' | null
>(null)
const [
deleteFolderConfirmationModalOpen,
setDeleteFolderConfirmationModalOpen
] = useState(false)
const [existedData, setExistedData] = useState<INotesEntry | null>(null)
function updateNotesEntries(): void {
setNotesEntries('loading')
fetch(
`${import.meta.env.VITE_API_HOST}/notes/entry/list/${subject}/${path}`
)
.then(async response => {
const data = await response.json()
setNotesEntries(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setNotesEntries('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateNotesEntries()
}, [path])
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-scroll px-8 md:px-12">
<DirectoryHeader
updateNotesEntries={updateNotesEntries}
setModifyFolderModalOpenType={setModifyFolderModalOpenType}
setExistedData={setExistedData}
/>
{(() => {
switch (notesEntries) {
case 'loading':
return <Loading />
case 'error':
return <Error message="Failed to fetch data from server." />
default:
return notesEntries.length > 0 ? (
<Directory
notesEntries={notesEntries}
setDeleteFolderConfirmationModalOpen={
setDeleteFolderConfirmationModalOpen
}
setModifyFolderModalOpenType={setModifyFolderModalOpenType}
setExistedData={setExistedData}
/>
) : (
<EmptyStateScreen
ctaContent="New Note"
icon="tabler:file-off"
title="Hmm... it seems a bit empty here."
description="Time to upload some notes!"
setModifyModalOpenType={() => {}}
/>
)
}
})()}
<ModifyFolderModal
openType={modifyFolderModalOpenType}
setOpenType={setModifyFolderModalOpenType}
existedData={existedData}
updateNotesEntries={updateNotesEntries}
/>
<DeleteFolderConfirmationModal
isOpen={deleteFolderConfirmationModalOpen}
closeModal={() => {
setExistedData(null)
setDeleteFolderConfirmationModalOpen(false)
}}
folderDetails={existedData}
updateNotesEntries={updateNotesEntries}
/>
</section>
)
}
export default NotesSubject

View File

@@ -1,88 +0,0 @@
/* eslint-disable multiline-ternary */
import React, { useState } from 'react'
import { toast } from 'react-toastify'
import { Icon } from '@iconify/react/dist/iconify.js'
import Modal from '../../../../components/general/Modal'
import { type INotesEntry } from '..'
function DeleteFolderConfirmationModal({
isOpen,
closeModal,
folderDetails,
updateNotesEntries
}: {
isOpen: boolean
closeModal: () => void
folderDetails: INotesEntry | null
updateNotesEntries: () => void
}): React.ReactElement {
const [loading, setLoading] = useState(false)
function deleteFolder(): void {
if (folderDetails === null) return
setLoading(true)
fetch(
`${import.meta.env.VITE_API_HOST}/notes/entry/delete/${folderDetails.id}`,
{
method: 'DELETE'
}
)
.then(async res => {
const data = await res.json()
if (res.ok) {
toast.info("Uhh, hopefully you truly didn't need that folder.")
closeModal()
updateNotesEntries()
return data
} else {
throw new Error(data.message)
}
})
.catch(err => {
toast.error("Oops! Couldn't delete the folder. Please try again.")
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
return (
<Modal isOpen={isOpen}>
<h1 className="truncate text-2xl font-semibold">
Are you sure you want to delete {folderDetails?.name}?
</h1>
<p className="mt-2 text-neutral-500">
This will delete the folder and all the notes inside it. This action is
irreversible!
</p>
<div className="mt-8 flex w-full justify-around gap-2">
<button
onClick={closeModal}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-800 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-neutral-700"
>
Cancel
</button>
<button
disabled={loading}
onClick={deleteFolder}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-red-600"
>
{loading ? (
<>
<span className="small-loader-light"></span>
</>
) : (
<>
<Icon icon="tabler:trash" className="h-5 w-5" />
DELETE
</>
)}
</button>
</div>
</Modal>
)
}
export default DeleteFolderConfirmationModal

View File

@@ -1,178 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import { Icon } from '@iconify/react/dist/iconify.js'
import { toast } from 'react-toastify'
import { useDebounce } from '@uidotdev/usehooks'
import Modal from '../../../../components/general/Modal'
import { useParams } from 'react-router'
import { type INotesEntry } from '..'
import Input from '../../../../components/general/Input'
function ModifyFolderModal({
openType,
setOpenType,
updateNotesEntries,
existedData
}: {
openType: 'create' | 'update' | null
setOpenType: React.Dispatch<React.SetStateAction<'create' | 'update' | null>>
updateNotesEntries: () => void
existedData: INotesEntry | null
}): React.ReactElement {
const {
workspace,
subject,
'*': path
} = useParams<{
workspace: string
subject: string
'*': string
}>()
const [loading, setLoading] = useState(false)
const [folderName, setFolderName] = useState('')
const innerOpenType = useDebounce(openType, openType === null ? 300 : 0)
function updateFolderName(e: React.ChangeEvent<HTMLInputElement>): void {
setFolderName(e.target.value)
}
function onSubmitButtonClick(): void {
if (folderName.trim().length === 0) {
toast.error('Please fill in all the fields.')
return
}
setLoading(true)
const entry = {
name: folderName.trim(),
workspace,
type: 'folder',
parent: path !== undefined ? path.split('/').pop() : '',
subject
}
fetch(
`${import.meta.env.VITE_API_HOST}/notes/entry/${innerOpenType}/folder` +
(innerOpenType === 'update' ? `/${existedData!.id}` : ''),
{
method: innerOpenType === 'create' ? 'PUT' : 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(entry)
}
)
.then(async res => {
const data = await res.json()
if (res.status !== 200) {
throw data.message
}
toast.success(
{
create: 'Yay! Folder created. Time to fill it up.',
update: 'Yay! Folder renamed.'
}[innerOpenType!]
)
setOpenType(null)
updateNotesEntries()
})
.catch(err => {
toast.error(
{
create: "Oops! Couldn't create the folder. Please try again.",
update: "Oops! Couldn't rename the folder. Please try again."
}[innerOpenType!] +
' Error: ' +
err
)
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
if (innerOpenType === 'update' && existedData !== null) {
setFolderName(existedData.name)
} else {
setFolderName('')
}
}, [innerOpenType, existedData])
return (
<>
<Modal isOpen={openType !== null}>
<div className="mb-8 flex items-center justify-between ">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-7 w-7"
/>
{
{
create: 'Create ',
update: 'Rename '
}[innerOpenType!]
}{' '}
folder
</h1>
<button
onClick={() => {
setOpenType(null)
}}
className="rounded-md p-2 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
<Input
name="Folder Name"
placeholder="My lovely folder"
icon="tabler:folder"
value={folderName}
updateValue={updateFolderName}
darker
additionalClassName="w-[40vw]"
/>
<button
disabled={loading}
className="mt-8 flex h-16 items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-custom-600 dark:text-neutral-800"
onClick={onSubmitButtonClick}
>
{!loading ? (
<>
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-5 w-5"
/>
{
{
create: 'CREATE',
update: 'RENAME'
}[innerOpenType!]
}
</>
) : (
<span className="small-loader-dark"></span>
)}
</button>
</Modal>
</>
)
}
export default ModifyFolderModal

View File

@@ -1,31 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { type INotesSubject } from './SubjectItem'
function CreateSubjectButton({
setModifySubjectModalOpenType,
setExistedData
}: {
setModifySubjectModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: React.Dispatch<React.SetStateAction<INotesSubject | null>>
}): React.ReactElement {
return (
<button
onClick={() => {
setModifySubjectModalOpenType('create')
setExistedData(null)
}}
className="relative flex h-full flex-col items-center justify-center gap-6 rounded-lg border-2 border-dashed border-neutral-400 p-8 hover:bg-neutral-200 dark:border-neutral-700 dark:hover:bg-neutral-800/20"
>
<Icon icon="tabler:folder-plus" className="h-16 w-16 text-neutral-500" />
<div className="text-2xl font-medium tracking-wide text-neutral-500">
Create subject
</div>
</button>
)
}
export default CreateSubjectButton

View File

@@ -1,74 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import { Link, useParams } from 'react-router-dom'
import HamburgerMenu from '../../../../components/general/HamburgerMenu'
import MenuItem from '../../../../components/general/HamburgerMenu/MenuItem'
export interface INotesSubject {
workspace: string
collectionId: string
collectionName: string
created: string
description: string
icon: string
id: string
title: string
updated: string
}
function SubjectItem({
subject,
setModifySubjectModalOpenType,
setExistedData,
setDeleteSubjectConfirmationModalOpen
}: {
subject: INotesSubject
setModifySubjectModalOpenType: React.Dispatch<
React.SetStateAction<'create' | 'update' | null>
>
setExistedData: (data: any) => void
setDeleteSubjectConfirmationModalOpen: (state: boolean) => void
}): React.ReactElement {
const { workspace } = useParams<{ workspace: string }>()
return (
<div className="group relative flex h-full w-full flex-col items-center rounded-lg bg-neutral-50 p-8 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-100 dark:bg-neutral-800/50 dark:hover:bg-neutral-700/50">
<Icon
icon={subject.icon}
className="pointer-events-none z-10 h-20 w-20 shrink-0 group-hover:text-custom-500"
/>
<h2 className="mt-8 text-center text-2xl font-medium uppercase tracking-widest">
{subject.title}
</h2>
<p className="mt-2 text-center text-sm text-neutral-500">
{subject.description}
</p>
<Link
to={`/notes/${workspace}/${subject.id}`}
className="absolute left-0 top-0 h-full w-full hover:bg-white/[0.05]"
/>
<HamburgerMenu position="absolute right-4 top-4 z-20">
<MenuItem
onClick={() => {
setExistedData(subject)
setModifySubjectModalOpenType('update')
}}
icon="tabler:edit"
text="Edit"
/>
<MenuItem
onClick={() => {
setExistedData(subject)
setDeleteSubjectConfirmationModalOpen(true)
}}
icon="tabler:trash"
text="Delete"
isRed
/>
</HamburgerMenu>
</div>
)
}
export default SubjectItem

View File

@@ -1,163 +0,0 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import ModuleHeader from '../../../components/general/ModuleHeader'
import { Icon } from '@iconify/react/dist/iconify.js'
import { useNavigate, useParams } from 'react-router'
import { type INotesWorkspace } from '..'
import { toast } from 'react-toastify'
import Error from '../../../components/general/Error'
import Loading from '../../../components/general/Loading'
import EmptyStateScreen from '../../../components/general/EmptyStateScreen'
import ModifySubjectModal from './modals/ModifySubjectModal'
import DeleteSubjectConfirmationModal from './modals/DeleteSubjectConfirmationModal'
import GoBackButton from '../../../components/general/GoBackButton'
import SubjectItem, { type INotesSubject } from './components/SubjectItem'
import CreateSubjectButton from './components/CreateSubjectButton'
function NotesCategory(): React.ReactElement {
const { workspace } = useParams<{ workspace: string }>()
const [titleData, setTitleData] = useState<
INotesWorkspace | 'error' | 'loading'
>('loading')
const [subjectsData, setSubjectsData] = useState<
INotesSubject[] | 'error' | 'loading'
>('loading')
const [modifySubjectModalOpenType, setModifySubjectModalOpenType] = useState<
'create' | 'update' | null
>(null)
const [
deleteSubjectConfirmationModalOpen,
setDeleteSubjectConfirmationModalOpen
] = useState(false)
const [existedData, setExistedData] = useState<INotesSubject | null>(null)
const navigate = useNavigate()
function updateTitleData(): void {
setTitleData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/notes/workspace/get/${workspace}`)
.then(async response => {
const data = await response.json()
setTitleData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setTitleData('error')
toast.error('Failed to fetch data from server.')
})
}
function updateSubjectList(): void {
setSubjectsData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/notes/subject/list/${workspace}`)
.then(async response => {
const data = await response.json()
setSubjectsData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setSubjectsData('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateTitleData()
updateSubjectList()
}, [workspace])
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-scroll px-8 md:px-12">
<GoBackButton
onClick={() => {
navigate('/notes')
}}
/>
<ModuleHeader
title={
<>
Notes -{' '}
{titleData === 'loading' ? (
<Icon icon="svg-spinners:180-ring" className="h-8 w-8" />
) : titleData === 'error' ? (
<span className="flex items-center gap-2 text-red-500">
<Icon
icon="tabler:alert-triangle"
className="mt-1 h-8 w-8 stroke-red-500 stroke-[2px]"
/>
Failed to fetch data
</span>
) : (
titleData.name
)}
</>
}
desc="A place to store all your involuntarily generated thoughts."
/>
<>
{(() => {
switch (subjectsData) {
case 'loading':
return <Loading />
case 'error':
return <Error message="Failed to fetch data from server." />
default:
return subjectsData.length > 0 ? (
<div className="grid grid-cols-[repeat(auto-fill,minmax(20rem,1fr))] items-center justify-center gap-4 py-8">
{subjectsData.map(subject => (
<SubjectItem
key={subject.id}
subject={subject}
setModifySubjectModalOpenType={
setModifySubjectModalOpenType
}
setDeleteSubjectConfirmationModalOpen={
setDeleteSubjectConfirmationModalOpen
}
setExistedData={setExistedData}
/>
))}
<CreateSubjectButton
setModifySubjectModalOpenType={
setModifySubjectModalOpenType
}
setExistedData={setExistedData}
/>
</div>
) : (
<EmptyStateScreen
title="A bit empty here. "
description="Create a new subject to start storing your notes."
icon="tabler:folder-off"
ctaContent="Create subject"
setModifyModalOpenType={setModifySubjectModalOpenType}
/>
)
}
})()}
</>
<DeleteSubjectConfirmationModal
isOpen={deleteSubjectConfirmationModalOpen}
closeModal={() => {
setDeleteSubjectConfirmationModalOpen(false)
}}
subjectDetails={existedData}
updateSubjectList={updateSubjectList}
/>
<ModifySubjectModal
openType={modifySubjectModalOpenType}
setOpenType={setModifySubjectModalOpenType}
existedData={existedData}
updateSubjectList={updateSubjectList}
/>
</section>
)
}
export default NotesCategory

View File

@@ -1,90 +0,0 @@
/* eslint-disable multiline-ternary */
import React, { useState } from 'react'
import { toast } from 'react-toastify'
import { Icon } from '@iconify/react/dist/iconify.js'
import Modal from '../../../../components/general/Modal'
import { type INotesSubject } from '../components/SubjectItem'
function DeleteSubjectConfirmationModal({
isOpen,
closeModal,
subjectDetails,
updateSubjectList
}: {
isOpen: boolean
closeModal: () => void
subjectDetails: INotesSubject | null
updateSubjectList: () => void
}): React.ReactElement {
const [loading, setLoading] = useState(false)
function deleteFolder(): void {
if (subjectDetails === null) return
setLoading(true)
fetch(
`${import.meta.env.VITE_API_HOST}/notes/subject/delete/${
subjectDetails.id
}`,
{
method: 'DELETE'
}
)
.then(async res => {
const data = await res.json()
if (res.ok) {
toast.info("Uhh, hopefully you truly didn't need that folder.")
closeModal()
updateSubjectList()
return data
} else {
throw new Error(data.message)
}
})
.catch(err => {
toast.error("Oops! Couldn't delete the folder. Please try again.")
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
return (
<Modal isOpen={isOpen}>
<h1 className="text-2xl font-semibold">
Are you sure you want to delete {subjectDetails?.title}?
</h1>
<p className="mt-2 text-neutral-500">
This will delete the subject and all the notes inside it. This action is
irreversible!
</p>
<div className="mt-8 flex w-full justify-around gap-2">
<button
onClick={closeModal}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-800 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-neutral-700"
>
Cancel
</button>
<button
disabled={loading}
onClick={deleteFolder}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-red-600"
>
{loading ? (
<>
<span className="small-loader-light"></span>
</>
) : (
<>
<Icon icon="tabler:trash" className="h-5 w-5" />
DELETE
</>
)}
</button>
</div>
</Modal>
)
}
export default DeleteSubjectConfirmationModal

View File

@@ -1,200 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable multiline-ternary */
import React, { useEffect, useState } from 'react'
import { Icon } from '@iconify/react/dist/iconify.js'
import { toast } from 'react-toastify'
import { useDebounce } from '@uidotdev/usehooks'
import Modal from '../../../../components/general/Modal'
import { useParams } from 'react-router'
import Input from '../../../../components/general/Input'
import IconInput from '../../../../components/general/IconSelector/IconInput'
import { type INotesSubject } from '../components/SubjectItem'
function ModifySubjectModal({
openType,
setOpenType,
updateSubjectList,
existedData
}: {
openType: 'create' | 'update' | null
setOpenType: React.Dispatch<React.SetStateAction<'create' | 'update' | null>>
updateSubjectList: () => void
existedData: INotesSubject | null
}): React.ReactElement {
const { workspace } = useParams<{ workspace: string }>()
const [loading, setLoading] = useState(false)
const [subjectName, setSubjectName] = useState('')
const [subjectDescription, setSubjectDescription] = useState('')
const [subjectIcon, setSubjectIcon] = useState('tabler:cube')
const innerOpenType = useDebounce(openType, openType === null ? 300 : 0)
function updateSubjectName(e: React.ChangeEvent<HTMLInputElement>): void {
setSubjectName(e.target.value)
}
function updateSubjectDescription(
e: React.ChangeEvent<HTMLInputElement>
): void {
setSubjectDescription(e.target.value)
}
function onSubmitButtonClick(): void {
if (
subjectName.trim().length === 0 ||
subjectIcon.trim().length === 0 ||
subjectDescription.trim().length === 0
) {
toast.error('Please fill in all the fields.')
return
}
setLoading(true)
const subject = {
title: subjectName.trim(),
description: subjectDescription.trim(),
icon: subjectIcon.trim(),
workspace
}
fetch(
`${import.meta.env.VITE_API_HOST}/notes/subject/${innerOpenType}` +
(innerOpenType === 'update' ? `/${existedData?.id}` : ''),
{
method: innerOpenType === 'create' ? 'PUT' : 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subject)
}
)
.then(async res => {
const data = await res.json()
if (res.status !== 200) {
throw data.message
}
toast.success(
{
create: 'Yay! Subject created. Time to fill it up.',
update: 'Yay! Subject updated.'
}[innerOpenType!]
)
setOpenType(null)
updateSubjectList()
})
.catch(err => {
toast.error(
{
create: "Oops! Couldn't create the subject. Please try again.",
update: "Oops! Couldn't update the subject. Please try again."
}[innerOpenType!] +
' Error: ' +
err
)
console.error(err)
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
if (innerOpenType === 'update' && existedData !== null) {
setSubjectName(existedData.title)
setSubjectDescription(existedData.description)
setSubjectIcon(existedData.icon)
} else {
setSubjectName('')
setSubjectDescription('')
setSubjectIcon('tabler:cube')
}
}, [innerOpenType, existedData])
return (
<>
<Modal isOpen={openType !== null}>
<div className="mb-8 flex items-center justify-between ">
<h1 className="flex items-center gap-3 text-2xl font-semibold">
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-7 w-7"
/>
{
{
create: 'Create ',
update: 'Update '
}[innerOpenType!]
}{' '}
subject
</h1>
<button
onClick={() => {
setOpenType(null)
}}
className="rounded-md p-2 text-neutral-500 transition-all hover:bg-neutral-200/50 hover:text-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<Icon icon="tabler:x" className="h-6 w-6" />
</button>
</div>
<Input
name="Subject name"
icon="tabler:book"
value={subjectName}
updateValue={updateSubjectName}
darker
additionalClassName="w-[40vw]"
placeholder="My Subject"
/>
<Input
name="Subject description"
icon="tabler:file-text"
value={subjectDescription}
updateValue={updateSubjectDescription}
darker
additionalClassName="w-[40vw] mt-6"
placeholder="The best subject in the world"
/>
<IconInput
name="Subject icon"
icon={subjectIcon}
setIcon={setSubjectIcon}
/>
<button
disabled={loading}
className="mt-8 flex h-16 items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 transition-all hover:bg-custom-600 dark:text-neutral-800"
onClick={onSubmitButtonClick}
>
{!loading ? (
<>
<Icon
icon={
{
create: 'tabler:plus',
update: 'tabler:pencil'
}[innerOpenType!]
}
className="h-5 w-5"
/>
{
{
create: 'CREATE',
update: 'UPDATE'
}[innerOpenType!]
}
</>
) : (
<span className="small-loader-dark"></span>
)}
</button>
</Modal>
</>
)
}
export default ModifySubjectModal

View File

@@ -1,82 +0,0 @@
import React, { useEffect, useState } from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { toast } from 'react-toastify'
import Loading from '../../components/general/Loading'
import Error from '../../components/general/Error'
import { Icon } from '@iconify/react/dist/iconify.js'
import { Link } from 'react-router-dom'
export interface INotesWorkspace {
collectionId: string
collectionName: string
created: string
icon: string
id: string
name: string
updated: string
}
function Notes(): React.ReactElement {
const [data, setData] = useState<INotesWorkspace[] | 'error' | 'loading'>(
'loading'
)
function updateNotesWorkspace(): void {
setData('loading')
fetch(`${import.meta.env.VITE_API_HOST}/notes/workspace/list`)
.then(async response => {
const data = await response.json()
setData(data.data)
if (response.status !== 200) {
throw data.message
}
})
.catch(() => {
setData('error')
toast.error('Failed to fetch data from server.')
})
}
useEffect(() => {
updateNotesWorkspace()
}, [])
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-scroll px-12">
<ModuleHeader
title="Notes"
desc="A place to store all your involuntarily generated thoughts."
/>
{(() => {
if (data === 'loading') {
return <Loading />
} else if (data === 'error') {
return <Error message="Failed to fetch data from server." />
} else {
return (
<div className="grid grid-cols-[repeat(auto-fit,minmax(20rem,1fr))] items-center justify-center gap-4 py-8">
{data.map(workspace => (
<Link
to={`/notes/${workspace.id}`}
key={workspace.id}
className="group flex h-full w-full flex-col items-center rounded-lg bg-neutral-50 p-16 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-neutral-100 dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<Icon
icon={workspace.icon}
className="h-20 w-20 shrink-0 group-hover:text-custom-500"
/>
<h2 className="mt-8 text-center text-2xl font-medium uppercase tracking-widest">
{workspace.name}
</h2>
</Link>
))}
</div>
)
}
})()}
</section>
)
}
export default Notes

View File

@@ -1,116 +0,0 @@
import React, { Fragment, useContext } from 'react'
import { PersonalizationContext } from '../../../providers/PersonalizationProvider'
import { Listbox, Transition } from '@headlessui/react'
import { Icon } from '@iconify/react/dist/iconify.js'
const COLORS = [
'red',
'pink',
'purple',
'deep-purple',
'indigo',
'blue',
'light-blue',
'cyan',
'teal',
'green',
'light-green',
'lime',
'yellow',
'amber',
'orange',
'deep-orange',
'brown',
'grey'
]
function ThemeColorSelector(): React.ReactElement {
const { themeColor, setThemeColor } = useContext(PersonalizationContext)
return (
<div className="mb-12 mt-4 flex w-full items-center justify-between">
<div>
<h3 className="block text-xl font-medium leading-normal">
Accent color
</h3>
<p className="text-neutral-500">
Select or customize your UI accent color.
</p>
</div>
<Listbox
value={themeColor}
onChange={color => {
setThemeColor(color)
}}
>
<div className="relative mt-1 w-48">
<Listbox.Button className="relative flex w-full items-center gap-2 rounded-lg border-[1.5px] border-neutral-300/50 py-4 pl-4 pr-10 text-left focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 sm:text-sm">
<span className="inline-block h-4 w-4 rounded-full bg-custom-500" />
<span className="mt-[-1px] block truncate">
{themeColor
.split('-')
.slice(1)
.map(e => e[0].toUpperCase() + e.slice(1))
.join(' ')}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<Icon
icon="tabler:chevron-down"
className="h-5 w-5 text-neutral-400"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
enter="transition ease-in duration-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-32 w-full divide-y divide-neutral-200 overflow-auto rounded-md bg-neutral-100 py-1 text-base shadow-lg focus:outline-none dark:divide-neutral-700 dark:bg-neutral-800/50 sm:text-sm">
{COLORS.map((color, i) => (
<Listbox.Option
key={i}
className={({ active }) =>
`relative cursor-pointer select-none transition-all p-4 flex items-center justify-between ${
active
? 'bg-neutral-200/50 dark:bg-neutral-800'
: '!bg-transparent'
}`
}
value={`theme-${color}`}
>
{({ selected }) => (
<>
<div>
<span className="flex items-center gap-2">
<span
className={`theme-${color} inline-block h-4 w-4 rounded-full bg-custom-500`}
/>
{color
.split('-')
.map(e => e[0].toUpperCase() + e.slice(1))
.join(' ')}
</span>
</div>
{selected && (
<Icon
icon="tabler:check"
className="texy-lg block text-gray-400 group-hover:text-custom-200"
/>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
)
}
export default ThemeColorSelector

View File

@@ -1,55 +0,0 @@
import React, { useContext } from 'react'
import { PersonalizationContext } from '../../../providers/PersonalizationProvider'
import { Icon } from '@iconify/react/dist/iconify.js'
function ThemeSelector(): React.ReactElement {
const { theme, setTheme } = useContext(PersonalizationContext)
return (
<div className="mt-4 w-full">
<h3 className="mt-6 block text-xl font-medium leading-normal">Theme</h3>
<p className="text-neutral-500">Select or customize your UI theme.</p>
<div className="mt-6 flex w-full flex-col gap-8 md:flex-row">
{[
{ name: 'System', Image: './mockup/system.png' },
{ name: 'Light', Image: './mockup/light.png' },
{ name: 'Dark', Image: './mockup/dark.png' }
].map(({ name, Image }) => (
<button
key={name}
type="button"
onClick={() => {
setTheme(name.toLowerCase() as 'system' | 'light' | 'dark')
}}
className="flex-1"
>
<div
className={`ring-2 ring-offset-8 ring-offset-neutral-50 dark:ring-offset-neutral-900 ${
theme === name.toLowerCase()
? 'ring-custom-500'
: 'ring-neutral-200 dark:ring-neutral-700'
} relative overflow-hidden rounded-lg lg:rounded-2xl`}
>
{theme === name.toLowerCase() && (
<Icon
icon="tabler:circle-check-filled"
className="absolute bottom-2 right-2.5 block h-6 w-6 text-xl text-custom-500"
/>
)}
<img src={Image} alt={name} className="w-full" />
</div>
<p
className={`mt-4 ${
theme === name.toLowerCase() && 'font-medium text-custom-500'
}`}
>
{name}
</p>
</button>
))}
</div>
</div>
)
}
export default ThemeSelector

View File

@@ -1,20 +0,0 @@
import React from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import ThemeSelector from './components/ThemeSelector'
import ThemeColorSelector from './components/ThemeColorSelector'
function Personalization(): React.ReactElement {
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-scroll px-8 sm:px-12">
<ModuleHeader
title="Personalisation"
desc="Customise your experience with the app."
/>
<ThemeSelector />
<div className="my-6 w-full border-b-[1.5px] border-neutral-200 dark:border-neutral-700" />
<ThemeColorSelector />
</section>
)
}
export default Personalization

View File

@@ -1,70 +0,0 @@
/* eslint-disable multiline-ternary */
import React, { useState } from 'react'
import { Icon } from '@iconify/react/dist/iconify.js'
export default function Timer(): React.ReactElement {
const [started, setStarted] = useState(false)
return (
<div className="flex min-h-0 w-full flex-1 flex-col items-center justify-center gap-12">
<div className="relative flex flex-col items-center justify-center">
<div
className="radial-progress absolute text-neutral-200 dark:text-neutral-800/50"
style={{
'--value': '100',
'--size': '28rem',
'--thickness': '20px'
}}
role="progressbar"
></div>
<div
className="radial-progress flex items-center justify-center text-amber-500"
style={{
'--value': '70',
'--size': '28rem',
'--thickness': '20px'
}}
role="progressbar"
>
<div className="z-[9999] mt-12 flex flex-col items-center gap-4 text-neutral-800 dark:text-neutral-100">
<span className="text-7xl font-medium tracking-widest">02:33</span>
<span className="text-lg font-medium uppercase tracking-widest text-amber-500">
short break
</span>
{started ? (
<span className="text-lg font-medium tracking-widest text-neutral-100">
#8
</span>
) : (
<button
onClick={() => {
setStarted(true)
}}
className="rounded-lg p-4 text-neutral-800 hover:bg-neutral-50 dark:bg-neutral-800/50 dark:text-neutral-100"
>
<Icon icon="tabler:play" className="h-8 w-8 shrink-0" />
</button>
)}
</div>
</div>
</div>
{started && (
<div className="flex items-center gap-6">
<button className="flex shrink-0 items-center gap-2 rounded-lg bg-amber-500 p-4 px-6 pr-7 font-semibold uppercase tracking-wider text-neutral-100 dark:text-neutral-800">
<Icon icon="tabler:pause" className="h-5 w-5 shrink-0" />
<span className="shrink-0">pause session</span>
</button>
<button
onClick={() => {
setStarted(false)
}}
className="flex shrink-0 items-center gap-2 rounded-lg bg-neutral-200 p-4 px-6 pr-7 font-semibold uppercase tracking-wider text-neutral-500 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700/50"
>
<Icon icon="tabler:square" className="h-5 w-5 shrink-0" />
<span className="shrink-0">end session</span>
</button>
</div>
)}
</div>
)
}

View File

@@ -1,60 +0,0 @@
/* eslint-disable multiline-ternary */
import React from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { Icon } from '@iconify/react/dist/iconify.js'
import Timer from './components/Timer'
export default function PomodoroTimer(): React.JSX.Element {
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-12">
<ModuleHeader
title="Pomodoro Timer"
desc="Increase your productivity by using the Pomodoro technique."
/>
<div className="mt-8 flex w-full flex-1">
<Timer />
<aside className="mb-12 w-2/6 overflow-y-scroll rounded-lg bg-neutral-50 p-6 dark:bg-neutral-800/50">
<h1 className="mb-2 flex items-center gap-2 text-2xl font-semibold">
<Icon icon="tabler:circle-check" className="text-3xl" />
<span className="ml-2">Things to do</span>
</h1>
<ul className="mt-6 flex flex-col gap-4">
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-indigo-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800">
Buy groceries
</div>
<div className="text-sm text-rose-500">
10:00 AM, 23 Nov 2023 (overdue 8 hours)
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-500 transition-all hover:border-orange-500" />
</li>
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-orange-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800">
Do homework
</div>
<div className="text-sm text-neutral-500">
00:00 AM, 31 Jan 2024
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-500 transition-all hover:border-orange-500" />
</li>
<li className="flex items-center justify-between gap-4 rounded-lg border-l-4 border-orange-500 bg-neutral-100 p-4 px-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<div className="flex flex-col gap-1">
<div className="font-semibold text-neutral-800">
Start doing revision for SPM Sejarah
</div>
<div className="text-sm text-neutral-500">
00:00 AM, 31 Jan 2024
</div>
</div>
<button className="h-6 w-6 rounded-full border-2 border-neutral-500 transition-all hover:border-orange-500" />
</li>
</ul>
</aside>
</div>
</section>
)
}

View File

@@ -1,102 +0,0 @@
import React from 'react'
import { Icon } from '@iconify/react'
import { Link } from 'react-router-dom'
function Kanban(): React.JSX.Element {
return (
<section className="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-y-auto pl-12">
<div className="flex flex-col gap-1 pr-12">
<Link
to="/projects"
className="mb-2 flex w-min items-center gap-2 rounded-lg p-2 pr-4 text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-100"
>
<Icon icon="tabler:chevron-left" className="text-xl" />
<span className="whitespace-nowrap text-lg font-medium">Go back</span>
</Link>
<div className="flex items-center justify-between">
<h1 className="flex items-center gap-4 text-3xl font-semibold dark:text-neutral-100">
<div className="rounded-lg bg-custom-500/20 p-3 text-custom-500 dark:bg-neutral-800">
<Icon icon="tabler:hammer" className="text-3xl" />
</div>
LifeForge.
<div className="ml-2 rounded-full bg-yellow-500/20 px-4 py-1.5 text-xs font-medium uppercase tracking-widest text-yellow-500 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)]">
In progress
</div>
</h1>
<div className="flex gap-2 rounded-lg p-2">
{[
'tabler:layout-columns',
'tabler:layout-list',
'tabler:arrow-autofit-content'
].map((icon, index) => (
<button
key={index}
className={`rounded-md p-4 ${
index === 0
? 'bg-neutral-300/50 dark:bg-neutral-700/50 dark:text-neutral-100'
: 'text-neutral-400 hover:bg-neutral-200/50 dark:hover:bg-neutral-700/50'
}`}
>
<Icon icon={icon} className="text-2xl" />
</button>
))}
</div>
</div>
</div>
<div className="mb-12 mt-8 flex min-h-0 min-w-0 flex-1 gap-4 overflow-x-auto overflow-y-hidden">
{[
['tabler:brain', 'Brainstorm', 'border-fuchsia-500'],
['tabler:settings', 'In Progress', 'border-yellow-500'],
['tabler:check', 'Done', 'border-green-500'],
['tabler:bug', 'Bugs', 'border-red-500']
].map(([icon, name, color], i) => (
<div
key={i}
className={`flex h-min max-h-full w-72 shrink-0 flex-col rounded-lg border-t-4 bg-neutral-50 p-6 pb-0 pr-4 dark:bg-neutral-800/50 ${color}`}
>
<div className="flex items-center justify-between">
<h3 className="flex items-center gap-4">
<Icon icon={icon} className="text-2xl" />
<span className="text-xl font-semibold text-neutral-800 dark:text-neutral-100">
{name}
</span>
</h3>
<button className="rounded-lg p-2 hover:bg-neutral-700/50">
<Icon icon="tabler:dots-vertical" className="text-xl" />
</button>
</div>
<ul className="mt-6 flex flex-col gap-2 overflow-y-auto pr-2">
{Array(Math.floor(Math.random() * 10))
.fill(0)
.map((_, index) => (
<li
key={index}
className="flex items-center gap-4 rounded-lg bg-neutral-100 p-4 shadow-[2px_2px_3px_rgba(0,0,0,0.05)] hover:bg-neutral-700/50 dark:bg-neutral-700/30"
>
<span className="text-neutral-800 dark:text-neutral-100">
{
[
'Lorem ipsum dolor sit amet',
'consectetur adipiscing elit',
'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
'Ut enim ad minim veniam'
][Math.floor(Math.random() * 4)]
}
</span>
</li>
))}
<li className="flex items-center justify-center">
<button className="mb-4 flex w-full items-center gap-2 rounded-lg border-neutral-500 p-4 pl-3 font-medium text-neutral-500 hover:bg-neutral-700/30">
<Icon icon="tabler:plus" className="text-xl" />
<span>Add a card</span>
</button>
</li>
</ul>
</div>
))}
</div>
</section>
)
}
export default Kanban

View File

@@ -1,297 +0,0 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/indent */
import React, { useEffect, useState } from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import SidebarTitle from '../../components/Sidebar/components/SidebarTitle'
import SidebarDivider from '../../components/Sidebar/components/SidebarDivider'
import { Icon } from '@iconify/react'
import { Link } from 'react-router-dom'
import SidebarItem from '../../components/Sidebar/components/SidebarItem'
import { faker } from '@faker-js/faker'
function shuffle(array: any[]): any[] {
let currentIndex = array.length
let randomIndex
// While there remain elements to shuffle.
while (currentIndex > 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex)
currentIndex--
// And swap it with the current element.
;[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex]
]
}
return array
}
function Projects(): React.JSX.Element {
const [icons, setIcons] = useState([])
useEffect(() => {
fetch('http://api.iconify.design/collection?prefix=tabler')
.then(async response => await response.json())
.then(data => {
setIcons(data.uncategorized)
})
.catch(() => {})
}, [])
return (
<section className="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-y-auto px-12">
<ModuleHeader
title="Projects"
desc="It's time to stop procrastinating."
/>
<div className="mb-12 mt-8 flex min-h-0 w-full flex-1">
<aside className="h-full w-1/4 overflow-hidden overflow-y-scroll rounded-lg bg-neutral-50 py-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<ul className="flex flex-col overflow-y-hidden hover:overflow-y-scroll">
<SidebarItem icon="tabler:list" name="All Projects" />
<SidebarItem icon="tabler:star-filled" name="Starred" />
<SidebarDivider />
<SidebarTitle name="category" />
{[
['tabler:world', 'Website'],
['tabler:device-mobile', 'Mobile App'],
['tabler:devices-pc', 'Desktop App']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-800">
<Icon icon={icon} className="h-6 w-6 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
<SidebarDivider />
<SidebarTitle name="status" />
{[
['tabler:zzz', 'Pending', 'bg-red-500'],
['tabler:circle-check', 'In Progress', 'bg-yellow-500'],
['tabler:circle-check', 'Completed', 'bg-green-500']
].map(([icon, name, color], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-200/50 dark:hover:bg-neutral-800">
<span className={`block h-8 w-1.5 rounded-full ${color}`} />
<Icon icon={icon} className="h-6 w-6 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
<SidebarDivider />
<SidebarTitle name="visibility" />
{[
['tabler:brand-open-source', 'Open Source'],
['tabler:briefcase', 'Private & Commercial']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-200/50 dark:hover:bg-neutral-800">
<Icon icon={icon} className="h-6 w-6 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
<SidebarDivider />
<SidebarTitle name="Technologies" />
{[
['simple-icons:react', 'React'],
['simple-icons:angular', 'Angular'],
['simple-icons:electron', 'Electron'],
['simple-icons:python', 'Python'],
['simple-icons:swift', 'Swift'],
['simple-icons:android', 'Android'],
['simple-icons:apple', 'iOS'],
['simple-icons:windows', 'Windows'],
['simple-icons:linux', 'Linux']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-200/50 dark:hover:bg-neutral-800">
<Icon icon={icon} className="h-5 w-5 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
</ul>
</aside>
<div className="ml-8 flex h-full flex-1 flex-col">
<div className="mx-4 flex items-center justify-between">
<h1 className="text-4xl font-semibold text-neutral-800 dark:text-neutral-100">
All Projects{' '}
<span className="text-base text-neutral-400">(10)</span>
</h1>
<button className="flex shrink-0 items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] hover:bg-custom-600 dark:text-neutral-800">
<Icon icon="tabler:plus" className="h-5 w-5 shrink-0" />
<span className="shrink-0">create</span>
</button>
</div>
<div className="mx-4 mt-6 flex items-center gap-4">
<search className="flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
placeholder="Search projects ..."
className="w-full bg-transparent text-neutral-500 placeholder:text-neutral-300 focus:outline-none"
/>
</search>
</div>
<div className="mt-6 flex flex-1 flex-col overflow-y-auto">
<ul className="flex flex-col">
{Array(10)
.fill(0)
.map((_, i) => (
<li
key={i}
className="m-4 mt-0 flex items-center gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<Link
to="./lifeforge"
className="flex w-full items-center justify-between gap-4"
>
<div className="flex w-2/5 items-center gap-4">
<div
className={`h-10 w-1 shrink-0 rounded-full ${
['bg-green-500', 'bg-yellow-500', 'bg-red-500'][
Math.floor(Math.random() * 3)
]
}`}
/>
<div
className={`h-12 w-12 overflow-hidden rounded-lg p-2 ${
[
'bg-red-500/20 text-red-500',
'bg-yellow-500/20 text-yellow-500',
'bg-green-500/20 text-green-500',
'bg-blue-500/20 text-blue-500',
'bg-indigo-500/20 text-indigo-500',
'bg-purple-500/20 text-purple-500',
'bg-pink-500/20 text-pink-500',
'bg-rose-500/20 text-rose-500',
'bg-fuchsia-500/20 text-fuchsia-500',
'bg-orange-500/20 text-orange-500',
'bg-cyan-500/20 text-cyan-500',
'bg-sky-500/20 text-sky-500',
'bg-lime-500/20 text-lime-500',
'bg-amber-500/20 text-amber-500',
'bg-emerald-500/20 text-emerald-500',
'bg-custom-500/20 text-custom-500'
][Math.floor(Math.random() * 16)]
}`}
>
<Icon
icon={`tabler:${
icons[
Math.floor(Math.random() * icons.length)
] as string
}`}
className="h-full w-full"
/>
</div>
<div className="flex flex-col items-start">
<div className="font-semibold text-neutral-800 dark:text-neutral-100">
{faker.commerce.productName()}
</div>
<div className="text-sm text-neutral-500">
{
['Website', 'Mobile App', 'Desktop App'][
Math.floor(Math.random() * 3)
]
}
</div>
</div>
</div>
<div className="flex items-center gap-4">
{(() => {
let randomLanguage = [
['simple-icons:react', 'React', 'text-sky-500'],
[
'simple-icons:typescript',
'TypeScript',
'text-blue-500'
],
[
'simple-icons:javascript',
'JavaScript',
'text-yellow-500'
],
['simple-icons:html5', 'HTML', 'text-orange-500'],
['simple-icons:css3', 'CSS', 'text-blue-500'],
[
'simple-icons:tailwindcss',
'Tailwind',
'text-cyan-500'
],
[
'simple-icons:nodedotjs',
'Node.js',
'text-green-500'
],
['simple-icons:express', 'Express', 'text-gray-500']
]
randomLanguage = shuffle(randomLanguage)
return randomLanguage
.slice(0, Math.floor(Math.random() * 5 + 2))
.map(([name, bg, color], index) => (
<div
key={index}
className={`flex items-center gap-2 rounded-full ${bg} text-sm font-medium ${color}`}
>
<Icon icon={name} className="h-5 w-5" />
</div>
))
})()}
</div>
<div className="flex items-center gap-4 ">
<Icon
icon="tabler:chevron-right"
className="h-5 w-5 stroke-[2px] text-neutral-400"
/>
</div>
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</section>
)
}
export default Projects

View File

@@ -1,128 +0,0 @@
import { faker } from '@faker-js/faker'
import { Icon } from '@iconify/react/dist/iconify.js'
import React from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import SidebarDivider from '../../components/Sidebar/components/SidebarDivider'
import SidebarItem from '../../components/Sidebar/components/SidebarItem'
import SidebarTitle from '../../components/Sidebar/components/SidebarTitle'
function ReferenceBooks(): React.JSX.Element {
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-12">
<ModuleHeader
title="Reference Books"
desc="A collection of reference books that accompany you on your learning journey."
/>
<div className="mb-12 mt-8 flex min-h-0 w-full flex-1">
<aside className="h-full w-1/4 overflow-y-scroll rounded-lg bg-neutral-50 py-4 dark:bg-neutral-800/50">
<ul className="flex flex-col overflow-y-hidden hover:overflow-y-scroll">
<SidebarItem icon="tabler:list" name="All books" />
<SidebarItem icon="tabler:star-filled" name="Starred" />
<SidebarDivider />
<SidebarTitle name="categories" />
{[
['tabler:math-integral-x', 'Calculus'],
['tabler:math-pi', 'Mathematics'],
['tabler:atom', 'Physics'],
['tabler:code', 'Computer Science']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-800">
<Icon icon={icon} className="h-5 w-5 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
<SidebarDivider />
<SidebarTitle name="languages" />
{[
['emojione-monotone:flag-for-china', 'Chinese'],
['emojione-monotone:flag-for-united-kingdom', 'English']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-800">
<Icon icon={icon} className="h-5 w-5 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
</ul>
</aside>
<div className="ml-12 flex h-full min-h-0 flex-1 flex-col">
<div className="flex items-center justify-between">
<h1 className="text-4xl font-semibold text-neutral-100">
All Books <span className="text-base text-neutral-400">(10)</span>
</h1>
<button className="flex shrink-0 items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-800">
<Icon icon="tabler:plus" className="h-5 w-5 shrink-0" />
<span className="shrink-0">upload</span>
</button>
</div>
<search className="mt-6 flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800/50">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-100" />
<input
type="text"
placeholder="Search books ..."
className="w-full bg-transparent text-neutral-100 placeholder:text-neutral-100 focus:outline-none"
/>
</search>
<ul className="mt-6 grid min-h-0 grid-cols-3 gap-6 gap-y-12 overflow-y-auto">
{Array(10)
.fill(0)
.map((_, i) => (
<li
key={i}
className="relative flex flex-col items-start rounded-lg"
>
<div className="flex h-72 w-full items-center justify-center rounded-lg bg-neutral-50 p-8 dark:bg-neutral-800/50">
<img
src={faker.image.imageUrl(300, 400, 'airport', true)}
alt={faker.lorem.sentence()}
className="h-full"
/>
</div>
<div className="mt-4 text-xl font-medium text-neutral-100">
{faker.commerce.productName()}
</div>
<div className="mt-2 text-sm font-medium text-neutral-400">
{faker.person.fullName()}
</div>
<div className="mt-6 flex w-full flex-col gap-4">
<button className="flex items-center justify-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-800">
<Icon icon="tabler:book" className="h-5 w-5 shrink-0" />
<span className="shrink-0">read</span>
</button>
<button className="flex items-center justify-center gap-2 rounded-lg bg-neutral-800 p-4 pr-5 font-semibold uppercase tracking-wider">
<Icon
icon="tabler:download"
className="h-5 w-5 shrink-0"
/>
<span className="shrink-0">download</span>
</button>
</div>
</li>
))}
</ul>
</div>
</div>
</section>
)
}
export default ReferenceBooks

View File

@@ -1,112 +0,0 @@
import React from 'react'
import ModuleHeader from '../../components/general/ModuleHeader'
import { Icon } from '@iconify/react/dist/iconify.js'
import SidebarItem from '../../components/Sidebar/components/SidebarItem'
import SidebarDivider from '../../components/Sidebar/components/SidebarDivider'
import SidebarTitle from '../../components/Sidebar/components/SidebarTitle'
import { faker } from '@faker-js/faker'
export default function Resources(): React.JSX.Element {
return (
<section className="flex h-full min-h-0 w-full flex-1 flex-col px-12">
<ModuleHeader
title="Resources"
desc="A collection of useful stuff for your coding journey."
/>
<div className="mb-12 mt-8 flex min-h-0 w-full flex-1">
<aside className="h-full w-1/4 overflow-y-scroll rounded-lg bg-neutral-50 py-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<ul className="flex flex-col overflow-y-hidden hover:overflow-y-scroll">
<SidebarItem icon="tabler:list" name="All Resources" />
<SidebarItem icon="tabler:star-filled" name="Starred" />
<SidebarDivider />
<SidebarTitle name="categories" />
{[
['simple-icons:react', 'React Libraries'],
['tabler:database', 'Databases'],
['tabler:device-desktop-code', 'UI Frameworks'],
['tabler:terminal', 'Command Line Tools'],
['tabler:server', 'Servers']
].map(([icon, name], index) => (
<li
key={index}
className="relative flex items-center gap-6 px-4 font-medium text-neutral-400 transition-all"
>
<div className="flex w-full items-center gap-6 whitespace-nowrap rounded-lg p-4 hover:bg-neutral-800">
<Icon icon={icon} className="h-5 w-5 shrink-0" />
<div className="flex w-full items-center justify-between">
{name}
</div>
<span className="text-sm">
{Math.floor(Math.random() * 10)}
</span>
</div>
</li>
))}
</ul>
</aside>
<div className="ml-12 flex h-full min-h-0 flex-1 flex-col">
<div className="flex items-center justify-between">
<h1 className="text-4xl font-semibold text-neutral-800 dark:text-neutral-100">
All Resources{' '}
<span className="text-base text-neutral-400">(10)</span>
</h1>
<button className="flex shrink-0 items-center gap-2 rounded-lg bg-custom-500 p-4 pr-5 font-semibold uppercase tracking-wider text-neutral-100 dark:text-neutral-800">
<Icon icon="tabler:plus" className="h-5 w-5 shrink-0" />
<span className="shrink-0">add new</span>
</button>
</div>
<search className="mt-6 flex w-full items-center gap-4 rounded-lg bg-neutral-50 p-4 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50">
<Icon icon="tabler:search" className="h-5 w-5 text-neutral-500" />
<input
type="text"
placeholder="Search resources ..."
className="w-full bg-transparent text-neutral-500 placeholder:text-neutral-400 focus:outline-none"
/>
</search>
<ul className="mt-6 flex min-h-0 flex-col gap-4 overflow-y-auto">
{Array(10)
.fill(0)
.map((_, i) => (
<li
key={i}
className="relative flex items-center justify-between gap-4 rounded-lg bg-neutral-50 p-6 shadow-[4px_4px_10px_0px_rgba(0,0,0,0.05)] dark:bg-neutral-800/50"
>
<div className="flex w-full flex-col gap-1">
{(() => {
const randomCategory = [
['simple-icons:react', 'React Libraries'],
['tabler:database', 'Databases'],
['tabler:device-desktop-code', 'UI Frameworks'],
['tabler:terminal', 'Command Line Tools'],
['tabler:server', 'Servers']
][Math.floor(Math.random() * 5)]
return (
<div
className={
'-mt-1 mb-1 flex items-center gap-2 font-medium text-neutral-500'
}
>
<Icon icon={randomCategory[0]} className="h-4 w-4" />
<span>{randomCategory[1]}</span>
</div>
)
})()}
<div className="text-lg font-semibold text-neutral-800">
{faker.lorem.words(Math.floor(Math.random() * 5) + 1)}
</div>
<p className="text-neutral-500">
{faker.lorem.paragraphs(1)}
</p>
</div>
<button className="absolute right-4 top-4 rounded-md p-2 text-neutral-100 hover:bg-neutral-700/30 hover:text-neutral-100">
<Icon icon="tabler:dots-vertical" className="h-5 w-5" />
</button>
</li>
))}
</ul>
</div>
</div>
</section>
)
}

Some files were not shown because too many files have changed in this diff Show More