diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..c8d2505 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +NEXT_PUBLIC_BASE_PATH= +NEXT_PUBLIC_API_URL=http://localhost:8004/api-ncr \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..398c142 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +NEXT_PUBLIC_BASE_PATH=/temp +NEXT_PUBLIC_API_URL=https://bysrvapp.mam.gov.tr/api-ncr \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore index ceaea36..2a9755a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,132 +1,13 @@ -# ---> Node -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +### NodeJS node_modules/ -jspm_packages/ +build/ +.next/ +package-lock.json -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ +# VSCode +.vscode/ -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +### OS Specific +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index b4dd661..1fc4c8d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,69 @@ -# webTemplate +# NCR Dashboard — Next.js Template -Nextjs Web Template \ No newline at end of file +`webNonConformanceReport` (CRA + react-router-dom v5 + Light Bootstrap +Dashboard) projesinden, gereksiz bağımlılıklar kaldırılarak türetilmiş güncel +Next.js (App Router, JavaScript) şablonu. + +## Kurulum + +```bash +npm install +cp .env.example .env.local # NEXT_PUBLIC_API_URL değerini kendi API adresinizle değiştirin +npm run dev +``` + +## Eskisiyle karşılaştırma + +| Eski proje | Yeni proje | +|---|---| +| Create React App + `react-scripts` | Next.js 15 (App Router) | +| `react-router-dom` v5 (`Switch`, `useHistory`) | Next.js dosya tabanlı routing (`app/`), `next/navigation` | +| `ProtectedRoute.js` (route bileşeni) | `components/auth/AuthGuard.jsx` (dashboard route grubunu sarar) | +| Bootstrap + `react-bootstrap` + `reactstrap` + el yazımı SCSS teması | Tailwind CSS + shadcn/ui birincil (Radix tabanlı, kendi bileşen kodunuz) | +| Ant Design (`antd`) — sadece Login sayfasında kullanılıyordu | shadcn/ui `Button`/`Input`/`Card` bileşenleri | +| `@fortawesome/fontawesome-free`, `nc-icon` seti | `lucide-react` (tree-shakeable, tek ikon seti) | +| `chartist`, `react-chartist` (kullanılmıyordu) | kaldırıldı — ihtiyaç olursa `recharts` eklenebilir | +| `gulp`, `gulp-append-prepend` (SCSS build script'leri) | kaldırıldı — Tailwind zaten derlemeyi hallediyor | +| `@tanstack/react-table`, `react-highlight-words`, `react-notification-alert`, `@react-hook/media-query`, `@emotion/*`, `ajv`, `jquery` (optional) | kaldırıldı — hiçbiri `src/` içinde kullanılmıyordu | +| `localStorage` üzerinden manuel `ncr-user` okuma/yazma her dosyada tekrarlanıyordu | `store/authSlice.js` içinde tek noktadan yönetiliyor (Redux Toolkit korundu) | +| `REACT_APP_BASE_PATH` her `Route`/`Link`'te elle birleştiriliyordu | `next.config.mjs` → `basePath` (tek noktadan, otomatik) | +| `REACT_APP_API_URL` + çıplak `fetch` | `NEXT_PUBLIC_API_URL` + `lib/axios.js` (tek `axios` instance, token interceptor) | + +## Yapı + +``` +src/ + app/ + layout.js kök layout, font + Redux Provider + login/page.js + not-authorized/page.js + not-found.js genel 404 + (dashboard)/ + layout.js AuthGuard + Sidebar + Navbar + Footer + page.js Ana sayfa + ncr-list/page.js + project/page.js rol korumalı (QUALITY, ADMINISTRATOR) + resource/page.js rol korumalı + person/page.js rol korumalı + components/ + ui/ shadcn/ui birincil bileşenleri (button, input, card, ...) + layout/ Sidebar, Navbar, Footer + auth/AuthGuard.jsx oturum + rol kontrolü + lib/ + routes.js sidebar + erişim kontrolü tek kaynağı + axios.js API istemcisi + utils.js `cn()` class birleştirici + store/ + store.js, authSlice.js Redux Toolkit +``` + +## Not + +- Kimlik doğrulama halen istemci tarafında `localStorage` (`ncr-user`) üzerinden + yürütülüyor — eski projeyle aynı model. Sunucu tarafı (middleware/cookie + tabanlı) koruma isterseniz `AuthGuard` yerine `middleware.js` + httpOnly + cookie'ye geçmeniz önerilir. +- `ncr-list`, `project`, `resource`, `person` sayfaları içerik eklemeniz için + boş bırakılmış placeholder'lardır. +- Backend'iniz `POST /auth/login` isteğine `{ token, fullName, role, ... }` + benzeri bir JSON dönüyorsa login akışı olduğu gibi çalışır. diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..abe04df --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..00cd3be --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + basePath: process.env.NEXT_PUBLIC_BASE_PATH || "", + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..8b5a580 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "temp", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@reduxjs/toolkit": "^2.5.0", + "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.469.0", + "next": "^16.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-redux": "^9.2.0", + "tailwind-merge": "^2.6.0" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "eslint-config-next": "^15.1.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..2ef30fc --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/src/app/(dashboard)/layout.js b/src/app/(dashboard)/layout.js new file mode 100644 index 0000000..fb54fac --- /dev/null +++ b/src/app/(dashboard)/layout.js @@ -0,0 +1,34 @@ +"use client"; + +import AuthGuard from "@/components/auth/AuthGuard"; +import Sidebar from "@/components/layout/Sidebar"; +import Navbar from "@/components/layout/Navbar"; +import Footer from "@/components/layout/Footer"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { useState } from "react"; + +export default function DashboardLayout({ children }) { + const [mobileOpen, setMobileOpen] = useState(false); + + return ( + +
+ + + + + setMobileOpen(false)} /> + + +
+ +
{children}
+
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/page.js b/src/app/(dashboard)/page.js new file mode 100644 index 0000000..dc6b970 --- /dev/null +++ b/src/app/(dashboard)/page.js @@ -0,0 +1,23 @@ +import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; + +export default function MainPage() { + return ( +
+

+ Template +

+

+ Purpose of the application +

+ + + + Notes + + Free places in the template are the samples. + + + +
+ ); +} diff --git a/src/app/(dashboard)/person/page.js b/src/app/(dashboard)/person/page.js new file mode 100644 index 0000000..58f8b6c --- /dev/null +++ b/src/app/(dashboard)/person/page.js @@ -0,0 +1,10 @@ +import PlaceholderPage from "@/components/layout/PlaceholderPage"; + +export default function PersonPage() { + return ( + + ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..c3e0576 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Nötr, soğuk beyaz zemin */ + --background: 40 14% 97%; + --foreground: 222 32% 12%; + + --card: 0 0% 100%; + --card-foreground: 222 32% 12%; + + --border: 220 14% 89%; + --input: 220 14% 89%; + --ring: 38 71% 51%; + + /* Amber/altın vurgu — kalibrasyon etiketi / uygunsuzluk uyarı rengi */ + --primary: 38 71% 51%; + --primary-foreground: 222 32% 12%; + + --secondary: 220 16% 94%; + --secondary-foreground: 222 32% 12%; + + --muted: 220 16% 94%; + --muted-foreground: 220 9% 46%; + + --accent: 38 71% 96%; + --accent-foreground: 38 71% 30%; + + --destructive: 4 62% 47%; + --destructive-foreground: 0 0% 100%; + + /* Koyu lacivert sidebar */ + --sidebar: 222 47% 9%; + --sidebar-foreground: 220 20% 88%; + --sidebar-muted: 220 14% 60%; + --sidebar-active: 38 71% 51%; + + --radius: 0.5rem; + + --font-sans: "Inter", system-ui, -apple-system, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground font-sans antialiased; + } +} diff --git a/src/app/layout.js b/src/app/layout.js new file mode 100644 index 0000000..12ce056 --- /dev/null +++ b/src/app/layout.js @@ -0,0 +1,30 @@ +import { Inter, JetBrains_Mono } from "next/font/google"; +import { Providers } from "./providers"; +import "./globals.css"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-sans", + display: "swap", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-mono", + display: "swap", +}); + +export const metadata = { + title: "Temp Title", + description: "Temp Description", +}; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} diff --git a/src/app/login/page.js b/src/app/login/page.js new file mode 100644 index 0000000..13028f8 --- /dev/null +++ b/src/app/login/page.js @@ -0,0 +1,77 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useDispatch } from "react-redux"; +import { setUser } from "@/store/authSlice"; +import api from "@/lib/axios"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; + +export default function LoginPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const dispatch = useDispatch(); + const router = useRouter(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(""); + setLoading(true); + try { + const { data } = await api.post("/auth/login", { email, password }); + dispatch(setUser(data)); + router.push("/"); + } catch (err) { + setError(err.response?.data?.message || "Login failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + Login Page + Login with email & PC login password + + +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ + {error &&

{error}

} + + +
+
+
+
+ ); +} diff --git a/src/app/not-authorized/page.js b/src/app/not-authorized/page.js new file mode 100644 index 0000000..29a5702 --- /dev/null +++ b/src/app/not-authorized/page.js @@ -0,0 +1,18 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; + +export default function NotAuthorizedPage() { + const router = useRouter(); + + return ( +
+

403 - Yetkiniz Yok

+

Bu sayfayı görüntüleme yetkiniz bulunmuyor.

+ +
+ ); +} diff --git a/src/app/not-found.js b/src/app/not-found.js new file mode 100644 index 0000000..3077c02 --- /dev/null +++ b/src/app/not-found.js @@ -0,0 +1,20 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; + +export default function NotFound() { + const router = useRouter(); + + return ( +
+

404 - Page Not Found

+

+ The page you are looking for does not exist or may have been moved.. +

+ +
+ ); +} diff --git a/src/app/providers.js b/src/app/providers.js new file mode 100644 index 0000000..0f3b0e7 --- /dev/null +++ b/src/app/providers.js @@ -0,0 +1,8 @@ +"use client"; + +import { Provider } from "react-redux"; +import { store } from "@/store/store"; + +export function Providers({ children }) { + return {children}; +} diff --git a/src/assets/img/MAM2.png b/src/assets/img/MAM2.png new file mode 100644 index 0000000..1090f8b Binary files /dev/null and b/src/assets/img/MAM2.png differ diff --git a/src/assets/img/sidebar-4.jpg b/src/assets/img/sidebar-4.jpg new file mode 100644 index 0000000..3085c7b Binary files /dev/null and b/src/assets/img/sidebar-4.jpg differ diff --git a/src/components/auth/AuthGuard.jsx b/src/components/auth/AuthGuard.jsx new file mode 100644 index 0000000..6ce4abc --- /dev/null +++ b/src/components/auth/AuthGuard.jsx @@ -0,0 +1,42 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useDispatch, useSelector } from "react-redux"; +import { hydrateFromStorage } from "@/store/authSlice"; +import { findRouteByPath, canAccessRoute } from "@/lib/routes"; + +export default function AuthGuard({ children }) { + const dispatch = useDispatch(); + const router = useRouter(); + const pathname = usePathname(); + const { user, initialized } = useSelector((state) => state.auth); + + useEffect(() => { + dispatch(hydrateFromStorage()); + }, [dispatch]); + + useEffect(() => { + if (!initialized) return; + + if (!user) { + router.replace("/login"); + return; + } + + const route = findRouteByPath(pathname); + if (route && !canAccessRoute(route, user.role)) { + router.replace("/not-authorized"); + } + }, [initialized, user, pathname, router]); + + if (!initialized || !user) { + return ( +
+ Yükleniyor... +
+ ); + } + + return children; +} diff --git a/src/components/layout/Footer.jsx b/src/components/layout/Footer.jsx new file mode 100644 index 0000000..2e85a8a --- /dev/null +++ b/src/components/layout/Footer.jsx @@ -0,0 +1,7 @@ +export default function Footer() { + return ( + + ); +} diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx new file mode 100644 index 0000000..1f73867 --- /dev/null +++ b/src/components/layout/Navbar.jsx @@ -0,0 +1,87 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useDispatch, useSelector } from "react-redux"; +import { Menu, LogOut, User } from "lucide-react"; +import { logout } from "@/store/authSlice"; +import { dashboardRoutes } from "@/lib/routes"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { SheetTrigger } from "@/components/ui/sheet"; + +function getPageTitle(pathname) { + const route = dashboardRoutes.find((r) => r.href === pathname); + return route?.label || "Main Page"; +} + +export default function Navbar() { + const pathname = usePathname(); + const router = useRouter(); + const dispatch = useDispatch(); + const user = useSelector((state) => state.auth.user); + + const initials = (user?.fullName || "?") + .split(" ") + .map((p) => p[0]) + .slice(0, 2) + .join("") + .toUpperCase(); + + const handleLogout = () => { + dispatch(logout()); + router.push("/login"); + }; + + return ( +
+
+ + + +

{getPageTitle(pathname)}

+
+ + + + + + + Hesabım + + {/* + + + Profile + + */} + + + Log out + + + +
+ ); +} diff --git a/src/components/layout/PlaceholderPage.jsx b/src/components/layout/PlaceholderPage.jsx new file mode 100644 index 0000000..2a9d4d8 --- /dev/null +++ b/src/components/layout/PlaceholderPage.jsx @@ -0,0 +1,12 @@ +import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; + +export default function PlaceholderPage({ title, description }) { + return ( + + + {title} + {description} + + + ); +} diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx new file mode 100644 index 0000000..1ec027e --- /dev/null +++ b/src/components/layout/Sidebar.jsx @@ -0,0 +1,77 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { useSelector } from "react-redux"; +import { cn } from "@/lib/utils"; +import { dashboardRoutes, canAccessRoute } from "@/lib/routes"; + +import backgroundImageSidebar from "@/assets/img/sidebar-4.jpg"; +import logo from "@/assets/img/MAM2.png"; + +export default function Sidebar({ onNavigate }) { + const pathname = usePathname(); + const user = useSelector((state) => state.auth.user); + + const links = dashboardRoutes.filter( + (route) => + route.showInSidebar !== false && canAccessRoute(route, user?.role), + ); + + return ( +
+ +
+ +
+ +
+ Logo +
+ + TÜBİTAK MAM + + + + + +
+
+
+ ); +} diff --git a/src/components/ui/avatar.jsx b/src/components/ui/avatar.jsx new file mode 100644 index 0000000..fede42e --- /dev/null +++ b/src/components/ui/avatar.jsx @@ -0,0 +1,40 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx new file mode 100644 index 0000000..ad6d9b6 --- /dev/null +++ b/src/components/ui/button.jsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-secondary", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-secondary", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.jsx b/src/components/ui/card.jsx new file mode 100644 index 0000000..1e8c7f0 --- /dev/null +++ b/src/components/ui/card.jsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }; diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx new file mode 100644 index 0000000..ff3d29a --- /dev/null +++ b/src/components/ui/dropdown-menu.jsx @@ -0,0 +1,66 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuContent = React.forwardRef( + ({ className, sideOffset = 6, align = "end", ...props }, ref) => ( + + + + ) +); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuLabel = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuLabel, +}; diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx new file mode 100644 index 0000000..812518d --- /dev/null +++ b/src/components/ui/input.jsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef(({ className, type, ...props }, ref) => { + return ( + + ); +}); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/label.jsx b/src/components/ui/label.jsx new file mode 100644 index 0000000..d344771 --- /dev/null +++ b/src/components/ui/label.jsx @@ -0,0 +1,19 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@/lib/utils"; + +const Label = React.forwardRef(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/separator.jsx b/src/components/ui/separator.jsx new file mode 100644 index 0000000..9f4b6c0 --- /dev/null +++ b/src/components/ui/separator.jsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef( + ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/components/ui/sheet.jsx b/src/components/ui/sheet.jsx new file mode 100644 index 0000000..797be82 --- /dev/null +++ b/src/components/ui/sheet.jsx @@ -0,0 +1,43 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Sheet = DialogPrimitive.Root; +const SheetTrigger = DialogPrimitive.Trigger; +const SheetClose = DialogPrimitive.Close; + +const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const SheetContent = React.forwardRef(({ className, children, ...props }, ref) => ( + + + + {children} + + + Kapat + + + +)); +SheetContent.displayName = DialogPrimitive.Content.displayName; + +export { Sheet, SheetTrigger, SheetClose, SheetContent }; diff --git a/src/lib/axios.js b/src/lib/axios.js new file mode 100644 index 0000000..260fc4f --- /dev/null +++ b/src/lib/axios.js @@ -0,0 +1,20 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { "Content-Type": "application/json" }, +}); + +// Kayıtlı kullanıcının token'ını (varsa) her isteğe ekler. +api.interceptors.request.use((config) => { + if (typeof window !== "undefined") { + const raw = localStorage.getItem("ncr-user"); + const user = raw ? JSON.parse(raw) : null; + if (user?.token) { + config.headers.Authorization = `Bearer ${user.token}`; + } + } + return config; +}); + +export default api; diff --git a/src/lib/routes.js b/src/lib/routes.js new file mode 100644 index 0000000..e749f41 --- /dev/null +++ b/src/lib/routes.js @@ -0,0 +1,28 @@ +import { LayoutGrid, ClipboardList, FolderKanban, Wrench, Users } from "lucide-react"; + +// Sidebar'da görünen ve erişim denetiminde kullanılan tek kaynak. +// roles: undefined -> herkes erişebilir. Aksi halde listelenen rollere sahip +// kullanıcılar erişebilir. +export const dashboardRoutes = [ + { + href: "/", + label: "Main Page", + icon: LayoutGrid, + showInSidebar: false, + }, + { + href: "/person", + label: "Users", + icon: Users, + roles: ["ADMINISTRATOR"], + }, +]; + +export function canAccessRoute(route, role) { + if (!route.roles) return true; + return route.roles.includes(role); +} + +export function findRouteByPath(path) { + return dashboardRoutes.find((r) => r.href === path); +} diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..378ccef --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,6 @@ +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/src/store/authSlice.js b/src/store/authSlice.js new file mode 100644 index 0000000..ac6cf44 --- /dev/null +++ b/src/store/authSlice.js @@ -0,0 +1,49 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const STORAGE_KEY = "ncr-user"; + +function readStoredUser() { + if (typeof window === "undefined") return null; + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } +} + +const authSlice = createSlice({ + name: "auth", + initialState: { + user: null, + // Sunucu tarafında render edilirken localStorage okunamadığı için + // gerçek kullanıcı durumu içinde client'ta doldurulur. + initialized: false, + }, + reducers: { + setUser(state, action) { + state.user = action.payload; + state.initialized = true; + if (typeof window !== "undefined") { + if (action.payload) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(action.payload)); + } else { + localStorage.removeItem(STORAGE_KEY); + } + } + }, + hydrateFromStorage(state) { + state.user = readStoredUser(); + state.initialized = true; + }, + logout(state) { + state.user = null; + if (typeof window !== "undefined") { + localStorage.removeItem(STORAGE_KEY); + } + }, + }, +}); + +export const { setUser, hydrateFromStorage, logout } = authSlice.actions; +export default authSlice.reducer; diff --git a/src/store/store.js b/src/store/store.js new file mode 100644 index 0000000..f01020c --- /dev/null +++ b/src/store/store.js @@ -0,0 +1,8 @@ +import { configureStore } from "@reduxjs/toolkit"; +import authReducer from "./authSlice"; + +export const store = configureStore({ + reducer: { + auth: authReducer, + }, +}); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..d1a1302 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,59 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + "./src/app/**/*.{js,jsx}", + "./src/components/**/*.{js,jsx}", + ], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar))", + foreground: "hsl(var(--sidebar-foreground))", + muted: "hsl(var(--sidebar-muted))", + active: "hsl(var(--sidebar-active))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["var(--font-sans)", "system-ui", "sans-serif"], + mono: ["var(--font-mono)", "ui-monospace", "monospace"], + }, + }, + }, + plugins: [], +};