first issue
This commit is contained in:
parent
851ca573f5
commit
adeb80c8e8
|
|
@ -0,0 +1,2 @@
|
||||||
|
NEXT_PUBLIC_BASE_PATH=
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8004/api-ncr
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
NEXT_PUBLIC_BASE_PATH=/temp
|
||||||
|
NEXT_PUBLIC_API_URL=https://bysrvapp.mam.gov.tr/api-ncr
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
|
|
@ -1,132 +1,13 @@
|
||||||
# ---> Node
|
### NodeJS
|
||||||
# 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
|
|
||||||
node_modules/
|
node_modules/
|
||||||
jspm_packages/
|
build/
|
||||||
|
.next/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
# VSCode
|
||||||
web_modules/
|
.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
|
||||||
|
|
|
||||||
70
README.md
70
README.md
|
|
@ -1,3 +1,69 @@
|
||||||
# webTemplate
|
# NCR Dashboard — Next.js Template
|
||||||
|
|
||||||
Nextjs Web Template
|
`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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -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 (
|
||||||
|
<AuthGuard>
|
||||||
|
<div className="flex h-screen overflow-hidden bg-background">
|
||||||
|
<aside className="hidden w-64 shrink-0 lg:block">
|
||||||
|
<Sidebar />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||||
|
<SheetContent className="p-0">
|
||||||
|
<Sidebar onNavigate={() => setMobileOpen(false)} />
|
||||||
|
</SheetContent>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 lg:p-6">{children}</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
</AuthGuard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default function MainPage() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl py-16 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Template
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Purpose of the application
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Card className="mt-8 text-left">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Notes</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Free places in the template are the samples.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import PlaceholderPage from "@/components/layout/PlaceholderPage";
|
||||||
|
|
||||||
|
export default function PersonPage() {
|
||||||
|
return (
|
||||||
|
<PlaceholderPage
|
||||||
|
title="Kullanıcılar"
|
||||||
|
description="Yalnızca ADMINISTRATOR rolleri erişebilir."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<html lang="tr">
|
||||||
|
<body className={`${inter.variable} ${jetbrainsMono.variable}`}>
|
||||||
|
<Providers>{children}</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
|
<Card className="w-full max-w-sm">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle>Login Page</CardTitle>
|
||||||
|
<CardDescription>Login with email & PC login password</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">E-Mail</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
{loading ? "Loading..." : "Login"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center gap-3 px-4 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold">403 - Yetkiniz Yok</h2>
|
||||||
|
<p className="text-muted-foreground">Bu sayfayı görüntüleme yetkiniz bulunmuyor.</p>
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Geri Dön
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center gap-3 px-4 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold">404 - Page Not Found</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The page you are looking for does not exist or may have been moved..
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "@/store/store";
|
||||||
|
|
||||||
|
export function Providers({ children }) {
|
||||||
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
|
|
@ -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 (
|
||||||
|
<div className="flex h-screen items-center justify-center bg-background text-sm text-muted-foreground">
|
||||||
|
Yükleniyor...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border px-4 py-4 text-center text-xs text-muted-foreground lg:px-6">
|
||||||
|
© {new Date().getFullYear()} Temp App · v1.0.0
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<header className="flex h-16 items-center justify-between border-b border-border bg-card px-4 lg:px-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SheetTrigger asChild className="lg:hidden">
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<Menu className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<h1 className="text-base font-semibold">{getPageTitle(pathname)}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-secondary">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="hidden text-left sm:block">
|
||||||
|
<span className="block font-medium leading-tight">
|
||||||
|
{user?.fullName}
|
||||||
|
</span>
|
||||||
|
<span className="block text-xs text-muted-foreground leading-tight">
|
||||||
|
{user?.role}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>Hesabım</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{/*
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
*/}
|
||||||
|
<DropdownMenuItem onClick={handleLogout} className="text-destructive">
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default function PlaceholderPage({ title, description }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="relative flex h-full flex-col overflow-hidden text-sidebar-foreground">
|
||||||
|
<Image
|
||||||
|
src={backgroundImageSidebar}
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
sizes="256px"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-sidebar/85" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex h-full flex-col">
|
||||||
|
<Link href="/" className="flex items-center gap-2 px-5 py-5">
|
||||||
|
<div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md bg-white/10">
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
fill
|
||||||
|
sizes="36px"
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold tracking-wide">
|
||||||
|
TÜBİTAK MAM
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="flex-1 space-y-0.5 px-3 py-2">
|
||||||
|
{links.map((route) => {
|
||||||
|
const Icon = route.icon;
|
||||||
|
const active = pathname === route.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={route.href}
|
||||||
|
href={route.href}
|
||||||
|
onClick={onNavigate}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-white/10 text-sidebar-active"
|
||||||
|
: "text-sidebar-muted hover:bg-white/10 hover:text-sidebar-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 shrink-0" />
|
||||||
|
{route.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="px-5 py-4 text-[11px] text-sidebar-muted"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-primary/15 text-xs font-medium text-primary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
|
@ -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 (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
|
||||||
|
|
@ -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) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-md",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-secondary data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
|
|
@ -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) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
||||||
|
|
@ -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) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
|
|
@ -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) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn("fixed inset-0 z-40 bg-black/40", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Portal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-y-0 left-0 z-50 h-full w-72 border-r border-border bg-sidebar p-0 shadow-lg outline-none",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm text-sidebar-muted hover:text-sidebar-foreground focus:outline-none">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Kapat</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SheetContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Sheet, SheetTrigger, SheetClose, SheetContent };
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
@ -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 <AuthInitializer /> 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;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import authReducer from "./authSlice";
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
auth: authReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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: [],
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue