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,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

View File

@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}

36
tasker-client/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6725
tasker-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
{
"name": "tasker-client",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/material": "^6.1.6",
"@mui/x-data-grid": "^7.22.0",
"@reduxjs/toolkit": "^2.3.0",
"axios": "^1.7.7",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"gantt-task-react": "^0.3.9",
"lucide-react": "^0.454.0",
"next": "15.0.2",
"numeral": "^2.0.6",
"react": "^18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"recharts": "^2.13.1",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/numeral": "^2.0.5",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"eslint": "^8",
"eslint-config-next": "15.0.2",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

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;

View File

@@ -0,0 +1,44 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
white: "#ffffff",
grey: {
100: "#f3f4f6",
200: "#e5e7eb",
300: "#d1d5db",
400: "#9ca3af",
500: "#6b7280",
600: "#4b5563",
700: "#374151",
800: "#1f2937",
},
blue: {
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
},
"dark-bg": "#101214",
"dark-secondary": "#1e2029",
"dark-tertiary": "#25262d",
"blue-primary": "#3b82f6",
"stroke-dark": "#2d3135",
},
},
},
plugins: [],
};
export default config;

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}