Initial front end

This commit is contained in:
2024-10-31 16:20:52 +02:00
commit 2f3045eea0
22 changed files with 7385 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
import React from "react";
import { Menu, Moon, Search, Settings, Sun } from "lucide-react";
import Link from "next/link";
import { useAppDispatch, useAppSelector } from "@/app/redux";
import { setIsDarkMode, setIsSidebarCollapsed } from "@/state";
const Navbar = () => {
const dispatch = useAppDispatch();
const isSidebarCollapsed = useAppSelector(
(state) => state.global.isSidebarCollapsed,
);
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
return (
<div className="flex items-center justify-between bg-white px-4 py-3 dark:bg-black">
<div className="flex items-center gap-8">
{!isSidebarCollapsed ? null : (
<button
onClick={() => dispatch(setIsSidebarCollapsed(!isSidebarCollapsed))}
>
<Menu className="h-8 w-8 dark:text-white" />
</button>
)}
<div className="relative flex h-min w-[200px]">
<Search className="absolute left-[6px] top-1/2 mr-2 h-5 w-5 -translate-y-1/2 transform cursor-pointer dark:text-white" />
<input
className="w-full rounded border-none bg-gray-100 p-2 pl-8 placeholder-gray-500 focus:border-transparent focus:outline-none dark:bg-gray-700 dark:text-white dark:placeholder-white"
type="search"
placeholder="Search..."
/>
</div>
</div>
{/* Icons */}
<div className="flex items-center">
<button
onClick={() => dispatch(setIsDarkMode(!isDarkMode))}
className={
isDarkMode
? "rounded p-2 dark:hover:bg-gray-700"
: "rounded p-2 hover:bg-gray-100"
}
>
{isDarkMode ? (
<Sun className="e-6 h-6 cursor-pointer dark:text-white" />
) : (
<Moon className="e-6 h-6 cursor-pointer dark:text-white" />
)}
</button>
<Link
href="settings"
className={
isDarkMode
? "h-min w-min rounded p-2 dark:hover:bg-gray-700"
: "h-min w-min rounded p-2 hover:bg-gray-100"
}
>
<Settings className="h-6 w-6 cursor-pointer dark:text-white" />
</Link>
<div className="ml-2 mr-2 hidden min-h-[2em] w-[0.1rem] bg-gray-200 md:inline-block"></div>
</div>
</div>
);
};
export default Navbar;

View File

@@ -0,0 +1,162 @@
"use client";
import React from "react";
import Image from "next/image";
import {
AlertCircle,
AlertOctagon,
AlertTriangle,
Briefcase,
ChevronDownCircleIcon,
ChevronDownSquareIcon,
ChevronUpCircleIcon,
ChevronUpSquareIcon,
Home,
Layers3,
LockIcon,
LucideIcon,
Search,
Settings,
ShieldAlert,
User,
Users,
X,
} from "lucide-react";
import { usePathname } from "next/navigation";
import { setIsSidebarCollapsed } from "@/state";
import { useAppDispatch, useAppSelector } from "@/app/redux";
import Link from "next/link";
const Sidebar = () => {
const [showProjects, setShowProjects] = React.useState(true);
const [showPriority, setShowPriority] = React.useState(true);
const dispatch = useAppDispatch();
const isSidebarCollapsed = useAppSelector(
(state) => state.global.isSidebarCollapsed,
);
return (
<div
className={`fixed z-40 flex h-full w-64 flex-col justify-between overflow-y-auto bg-white shadow-xl transition-all duration-300 dark:bg-black ${isSidebarCollapsed ? "hidden w-0" : "w-64"}`}
>
<div className="flex h-full w-full flex-col justify-start">
<div className="z-50 flex min-h-[56px] w-64 items-center justify-between bg-white px-6 pt-3 dark:bg-black">
<div className="text-xl font-bold text-gray-800 dark:text-white">
Tasker.IO
</div>
{isSidebarCollapsed ? null : (
<button
className="py-3"
onClick={() =>
dispatch(setIsSidebarCollapsed(!isSidebarCollapsed))
}
>
<X className="h-6 w-6 text-gray-800 hover:text-gray-500 dark:text-white" />
</button>
)}
</div>
<div className="flex items-center gap-5 border-y-[1.5px] border-gray-200 px-8 py-4 dark:border-gray-700">
<Image src="/logo.png" alt="logo" width={40} height={40} />
<div>
<h3 className="text-md font-bold tracking-widest dark:text-gray-200">
Tasker.IO
</h3>
<div className="mt-1 flex items-start gap-2">
<LockIcon className="mt-[0.1rem] h-3 w-3 text-gray-500 dark:text-gray-400" />
<p className="text-xs text-gray-500">Private</p>
</div>
</div>
</div>
<nav className="z-10 w-full">
<SidebarLink icon={Home} label="Home" href="/" />
<SidebarLink icon={Briefcase} label="Timeline" href="/timeline" />
<SidebarLink icon={Search} label="Search" href="/search" />
<SidebarLink icon={Settings} label="Settings" href="/settings" />
<SidebarLink icon={User} label="Users" href="/users" />
<SidebarLink icon={Users} label="Teams" href="/teams" />
</nav>
<button
onClick={() => setShowProjects((prev) => !prev)}
className="flex w-full items-center justify-between px-8 py-3 text-gray-500"
>
<span className="">Projects</span>
{showProjects ? (
<ChevronDownCircleIcon className="h-5 w-5" />
) : (
<ChevronUpCircleIcon className="h-5 w-5" />
)}
</button>
<button
onClick={() => setShowPriority((prev) => !prev)}
className="flex w-full items-center justify-between px-8 py-3 text-gray-500"
>
<span className="">Priority</span>
{showPriority ? (
<ChevronDownSquareIcon className="h-5 w-5" />
) : (
<ChevronUpSquareIcon className="h-5 w-5" />
)}
</button>
{showPriority && (
<>
<SidebarLink
icon={AlertCircle}
label="Urgent"
href="/priority/urgent"
/>
<SidebarLink
icon={ShieldAlert}
label="High"
href="/priority/high"
/>
<SidebarLink
icon={AlertTriangle}
label="Medium"
href="/priority/medium"
/>
<SidebarLink icon={AlertOctagon} label="Low" href="/priority/low" />
<SidebarLink
icon={Layers3}
label="Backlog"
href="/priority/backlog"
/>
</>
)}
</div>
</div>
);
};
interface SidebarLinkProps {
href: string;
icon: LucideIcon;
label: string;
}
const SidebarLink = ({ href, icon: Icon, label }: SidebarLinkProps) => {
const pathname = usePathname();
const isActive = pathname === href || (pathname === "/" && href === "/home");
return (
<Link href={href} className="w-full">
<div
className={`relative flex cursor-pointer items-center gap-3 transition-colors hover:bg-gray-100 dark:bg-black dark:hover:bg-gray-700 ${
isActive ? "bg-gray-100 text-white dark:bg-gray-600" : ""
} justify-start px-8 py-3`}
>
{isActive && (
<div className="absolute left-0 top-0 h-[100%] w-[5px] bg-blue-200" />
)}
<Icon className="h-6 w-6 text-gray-800 dark:text-gray-100" />
<span className={`font-medium text-gray-800 dark:text-gray-100`}>
{label}
</span>
</div>
</Link>
);
};
export default Sidebar;

View File

@@ -0,0 +1,43 @@
"use client";
import React, { useEffect } from "react";
import Navbar from "@/app/(components)/Navbar";
import Sidebar from "@/app/(components)/Sidebar";
import StoreProvider, { useAppSelector } from "./redux";
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
const isSidebarCollapsed = useAppSelector(
(state) => state.global.isSidebarCollapsed,
);
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
});
return (
<div className="flex min-h-screen w-full bg-gray-50 text-gray-900">
<Sidebar />
<main
className={`flex w-full flex-col bg-gray-50 dark:bg-dark-bg ${isSidebarCollapsed ? "" : "md:pl-64"}`}
>
<Navbar />
{children}
</main>
</div>
);
};
const DashboardWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<StoreProvider>
<DashboardLayout>{children}</DashboardLayout>
</StoreProvider>
);
};
export default DashboardWrapper;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#root,
.app {
height: 100%;
width: 100%;
@apply text-sm;
@apply bg-gray-500;
@apply text-gray-900;
}

View File

@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import DashboardWrapper from "./dashboardWrapper";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<DashboardWrapper>{children}</DashboardWrapper>
</body>
</html>
);
}

View File

@@ -0,0 +1,7 @@
export default function Home() {
return (
<main className="flex flex-col items-center justify-center h-screen">
New
</main>
);
}

View File

@@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import { useRef } from "react";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import {
TypedUseSelectorHook,
useDispatch,
useSelector,
Provider,
} from "react-redux";
import globalReducer from "@/state";
import { api } from "@/state/api";
import { setupListeners } from "@reduxjs/toolkit/query";
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import createWebStorage from "redux-persist/lib/storage/createWebStorage";
/* REDUX PERSISTENCE */
const createNoopStorage = () => {
return {
getItem(_key: any) {
return Promise.resolve(null);
},
setItem(_key: any, value: any) {
return Promise.resolve(value);
},
removeItem(_key: any) {
return Promise.resolve();
},
};
};
const storage =
typeof window === "undefined"
? createNoopStorage()
: createWebStorage("local");
const persistConfig = {
key: "root",
storage,
whitelist: ["global"],
};
const rootReducer = combineReducers({
global: globalReducer,
[api.reducerPath]: api.reducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
/* REDUX STORE */
export const makeStore = () => {
return configureStore({
reducer: persistedReducer,
middleware: (getDefault) =>
getDefault({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(api.middleware),
});
};
/* REDUX TYPES */
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
/* PROVIDER */
export default function StoreProvider({
children,
}: {
children: React.ReactNode;
}) {
const storeRef = useRef<AppStore>();
if (!storeRef.current) {
storeRef.current = makeStore();
setupListeners(storeRef.current.dispatch);
}
const persistor = persistStore(storeRef.current);
return (
<Provider store={storeRef.current}>
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
</Provider>
);
}

View File

@@ -0,0 +1,10 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL }),
reducerPath: "api",
tagTypes: ["Task"],
endpoints: (builder) => ({}),
});
export const {} = api;

View File

@@ -0,0 +1,27 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface initialStateType {
isSidebarCollapsed: boolean;
isDarkMode: boolean;
}
const initialState = {
isSidebarCollapsed: false,
isDarkMode: false,
};
export const globalSlice = createSlice({
name: "global",
initialState,
reducers: {
setIsSidebarCollapsed: (state, action: PayloadAction<boolean>) => {
state.isSidebarCollapsed = action.payload;
},
setIsDarkMode: (state, action: PayloadAction<boolean>) => {
state.isDarkMode = action.payload;
},
},
});
export const { setIsSidebarCollapsed, setIsDarkMode } = globalSlice.actions;
export default globalSlice.reducer;