Skip to content

Commit 51434f0

Browse files
committed
for production
1 parent 5a0ee95 commit 51434f0

24 files changed

Lines changed: 5606 additions & 10268 deletions

README.md

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
A production-ready approvals system built with Next.js, featuring role-based access control for CEO and Executive users. Executives create requests with multiple suggestions, and CEOs review and make decisions.
44

5+
## 🧩 Project Idea (What and Why)
6+
7+
Organizations routinely need clear, auditable decisions on proposals (budgets, policies, purchases). Email threads and chat messages are hard to track and easy to lose. This project provides a focused approvals workflow where:
8+
9+
- Executives submit a request with 2–10 concrete suggestions/options.
10+
- A CEO reviews the context and either chooses an option, writes a custom decision, or rejects the request.
11+
- Every action is captured in an immutable audit trail for accountability and compliance.
12+
13+
The result is a transparent, structured, and fast decision-making process with role-based access, localization (AR/EN), and a modern, responsive UI.
14+
515
## 🚀 Features
616

717
### Core Functionality
@@ -183,30 +193,110 @@ Test coverage includes:
183193

184194
## 🚀 Deployment
185195

186-
### Environment Variables
196+
This guide covers production deployment using Yarn, Docker, and Vercel.
197+
198+
### 1) Environment Variables
199+
Create `.env` from `env.example` and set the following:
200+
187201
```bash
188202
DATABASE_URL="postgresql://user:pass@host:port/db"
189203
NEXTAUTH_SECRET="your-production-secret"
190204
NEXTAUTH_URL="https://your-domain.com"
191205
NODE_ENV="production"
192206
```
193207

194-
### Build and Deploy
208+
Notes:
209+
- Set `NEXTAUTH_URL` to your public URL (e.g. `https://app.example.com`).
210+
- Generate a strong `NEXTAUTH_SECRET` (e.g. `openssl rand -base64 32`).
211+
- Ensure the database is reachable from your hosting environment.
212+
213+
### 2) Production Build & Run (Node.js server)
214+
195215
```bash
196-
# Build the application
197-
npm run build
216+
# Install deps
217+
yarn install --frozen-lockfile
218+
219+
# Generate Prisma client
220+
yarn prisma:generate
198221

199-
# Start production server
200-
npm start
222+
# Apply migrations
223+
yarn prisma:deploy
224+
225+
# Build Next.js (standalone output is enabled in next.config.js)
226+
yarn build
227+
228+
# Start server
229+
yarn start
230+
```
231+
232+
Optional: systemd service (Linux)
233+
234+
```ini
235+
[Unit]
236+
Description=Approvals Next.js
237+
After=network.target
238+
239+
[Service]
240+
Type=simple
241+
WorkingDirectory=/var/www/approvals-nextjs
242+
Environment=NODE_ENV=production
243+
EnvironmentFile=/var/www/approvals-nextjs/.env
244+
ExecStart=/usr/bin/yarn start
245+
Restart=always
246+
RestartSec=5
247+
248+
[Install]
249+
WantedBy=multi-user.target
201250
```
202251

203-
### Docker Production
252+
### 3) Docker Deployment
253+
254+
Build and run the standalone server image:
255+
204256
```bash
205-
# Build production image
257+
# Build image
206258
docker build -t approvals-system .
207259

208-
# Run with environment variables
209-
docker run -p 3000:3000 --env-file .env approvals-system
260+
# Run container
261+
docker run --name approvals \
262+
--env-file .env \
263+
-p 3000:3000 \
264+
approvals-system
265+
```
266+
267+
With Docker Compose (database + app):
268+
269+
```bash
270+
# Start services in background
271+
yarn docker:up
272+
273+
# Apply migrations inside the app container (first run)
274+
docker exec -it $(docker ps -qf name=approvals) yarn prisma:deploy
275+
```
276+
277+
### 4) Vercel Deployment
278+
279+
Vercel is well-suited for Next.js deployments:
280+
- Connect your Git repository in Vercel.
281+
- Set environment variables in the Vercel dashboard (`DATABASE_URL`, `NEXTAUTH_SECRET`, `NEXTAUTH_URL`).
282+
- Ensure your database allows connections from Vercel (or use a managed Postgres like Neon/Supabase/PlanetScale compatible).
283+
284+
Prisma on Vercel:
285+
- Use `prisma migrate deploy` during build via a Vercel Build Step or CI step before deployment.
286+
- Alternatively, run migrations from a CI/CD pipeline or a one-off script targeting the same database.
287+
288+
No extra configuration is required for Next.js 15 app router. This project already sets `output: 'standalone'`.
289+
290+
### 5) Health Checks
291+
292+
After deployment, verify:
293+
294+
```bash
295+
# App responds
296+
curl -I https://your-domain.com | head -n 1
297+
298+
# Sign-in route (localized)
299+
curl -I https://your-domain.com/ar/sign-in | head -n 1
210300
```
211301

212302
## 🔧 Development Scripts
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useEffect } from 'react'
44
import { useSession } from 'next-auth/react'
5+
import { useTranslations } from 'next-intl'
56
import { MainLayout } from '@/components/layout/main-layout'
67
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
78
import { Button } from '@/components/ui/button'
@@ -17,6 +18,7 @@ import { type RequestListItem, type RequestStatus, type PaginatedResponse, type
1718

1819
export default function DashboardPage() {
1920
const { data: session } = useSession()
21+
const t = useTranslations('dashboard')
2022
const [requests, setRequests] = useState<RequestListItem[]>([])
2123
const [loading, setLoading] = useState(true)
2224
const [error, setError] = useState<string | null>(null)

app/[locale]/layout.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {NextIntlClientProvider} from 'next-intl'
2+
import {getMessages, setRequestLocale} from 'next-intl/server'
3+
4+
interface LocaleLayoutProps {
5+
children: React.ReactNode
6+
params: { locale: string }
7+
}
8+
9+
export default async function LocaleLayout({
10+
children,
11+
params
12+
}: LocaleLayoutProps) {
13+
const { locale } = params
14+
setRequestLocale(locale)
15+
16+
const isRTL = locale === 'ar'
17+
const messages = await getMessages()
18+
19+
return (
20+
<NextIntlClientProvider messages={messages} locale={locale}>
21+
<div dir={isRTL ? 'rtl' : 'ltr'}>{children}</div>
22+
</NextIntlClientProvider>
23+
)
24+
}
25+
26+
export function generateStaticParams() {
27+
return [{ locale: 'en' }, { locale: 'ar' }]
28+
}

app/[locale]/page.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Link from 'next/link'
2+
import {getTranslations} from 'next-intl/server'
3+
4+
interface HomePageProps {
5+
params: { locale: string }
6+
}
7+
8+
export default async function HomePage({ params }: HomePageProps) {
9+
const { locale } = params
10+
const tNav = await getTranslations('navigation')
11+
const tCommon = await getTranslations('common')
12+
const tAuth = await getTranslations('auth')
13+
14+
return (
15+
<main className="min-h-[80vh] bg-gradient-to-b from-white to-slate-50 py-16">
16+
<div className="container mx-auto px-4">
17+
<section className="text-center max-w-3xl mx-auto">
18+
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight">
19+
{tNav('appTitle')}
20+
</h1>
21+
<p className="mt-4 text-muted-foreground text-lg">
22+
{locale === 'ar'
23+
? 'أنشئ الطلبات ووافق عليها بسهولة مع سجل تدقيق كامل.'
24+
: 'Create and approve requests with a complete audit trail.'}
25+
</p>
26+
27+
<div className="mt-8 flex items-center justify-center gap-3">
28+
<Link
29+
href={`/${locale}/sign-in`}
30+
className="inline-flex items-center rounded-md bg-primary px-5 py-2.5 text-white shadow hover:opacity-95 focus:outline-none focus:ring-2 focus:ring-primary/30"
31+
>
32+
{tAuth('signIn')}
33+
</Link>
34+
<Link
35+
href={`/${locale}/my-requests`}
36+
className="inline-flex items-center rounded-md border px-5 py-2.5 text-foreground hover:bg-secondary"
37+
>
38+
{locale === 'ar' ? 'طلباتي' : 'My Requests'}
39+
</Link>
40+
</div>
41+
</section>
42+
43+
<section className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
44+
<div className="rounded-lg border bg-card p-6 shadow-sm">
45+
<h3 className="text-lg font-semibold">
46+
{locale === 'ar' ? 'قرارات مرنة' : 'Flexible Decisions'}
47+
</h3>
48+
<p className="mt-2 text-sm text-muted-foreground">
49+
{locale === 'ar'
50+
? 'اختر اقتراحاً أو أدخل قراراً مخصصاً أو ارفض الطلب.'
51+
: 'Choose a suggestion, enter a custom decision, or reject.'}
52+
</p>
53+
</div>
54+
55+
<div className="rounded-lg border bg-card p-6 shadow-sm">
56+
<h3 className="text-lg font-semibold">
57+
{locale === 'ar' ? 'تتبع كامل' : 'Full Traceability'}
58+
</h3>
59+
<p className="mt-2 text-sm text-muted-foreground">
60+
{locale === 'ar'
61+
? 'سجل تدقيق يوضح جميع الإجراءات بالتوقيتات.'
62+
: 'Audit trail capturing every action with timestamps.'}
63+
</p>
64+
</div>
65+
66+
<div className="rounded-lg border bg-card p-6 shadow-sm">
67+
<h3 className="text-lg font-semibold">
68+
{locale === 'ar' ? 'واجهة حديثة' : 'Modern UI'}
69+
</h3>
70+
<p className="mt-2 text-sm text-muted-foreground">
71+
{locale === 'ar'
72+
? 'مصمم بـ Tailwind و shadcn/ui مع دعم RTL.'
73+
: 'Built with Tailwind and shadcn/ui with RTL support.'}
74+
</p>
75+
</div>
76+
</section>
77+
78+
<div className="mt-14 text-center text-sm text-muted-foreground">
79+
<span className="rounded bg-secondary px-2 py-1">
80+
{tCommon('confirm')}{locale.toUpperCase()}
81+
</span>
82+
</div>
83+
</div>
84+
</main>
85+
)
86+
}
Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import { signIn } from 'next-auth/react'
55
import { useRouter } from 'next/navigation'
66
import { useForm } from 'react-hook-form'
77
import { zodResolver } from '@hookform/resolvers/zod'
8+
import { useTranslations, useLocale } from 'next-intl'
89
import { Button } from '@/components/ui/button'
910
import { Input } from '@/components/ui/input'
1011
import { Label } from '@/components/ui/label'
1112
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
1213
import { LoadingSpinner } from '@/components/ui/loading-spinner'
14+
import { LanguageSwitcher } from '@/components/ui/language-switcher'
1315
import { signInSchema, type SignInInput } from '@/lib/validations'
1416

1517
export default function SignInPage() {
1618
const [error, setError] = useState<string | null>(null)
1719
const [isLoading, setIsLoading] = useState(false)
1820
const router = useRouter()
21+
const t = useTranslations('auth')
22+
const tNav = useTranslations('navigation')
23+
const locale = useLocale()
1924

2025
const {
2126
register,
@@ -37,12 +42,12 @@ export default function SignInPage() {
3742
})
3843

3944
if (result?.error) {
40-
setError('Invalid email or password')
45+
setError(t('invalidCredentials'))
4146
} else {
4247
router.refresh()
4348
}
4449
} catch (err) {
45-
setError('An unexpected error occurred')
50+
setError(t('invalidCredentials'))
4651
} finally {
4752
setIsLoading(false)
4853
}
@@ -52,25 +57,28 @@ export default function SignInPage() {
5257
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
5358
<div className="max-w-md w-full space-y-8">
5459
<div className="text-center">
55-
<h1 className="text-3xl font-bold text-gray-900">Approvals System</h1>
56-
<p className="mt-2 text-gray-600">Sign in to your account</p>
60+
<div className="flex justify-between items-center mb-4">
61+
<h1 className="text-3xl font-bold text-gray-900">{tNav('appTitle')}</h1>
62+
<LanguageSwitcher />
63+
</div>
64+
<p className="mt-2 text-gray-600">{t('signIn')}</p>
5765
</div>
5866

5967
<Card>
6068
<CardHeader>
61-
<CardTitle>Sign In</CardTitle>
69+
<CardTitle>{t('signIn')}</CardTitle>
6270
<CardDescription>
63-
Enter your credentials to access the system
71+
{t('enterCredentials')}
6472
</CardDescription>
6573
</CardHeader>
6674
<CardContent>
6775
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
6876
<div>
69-
<Label htmlFor="email">Email</Label>
77+
<Label htmlFor="email">{t('email')}</Label>
7078
<Input
7179
id="email"
7280
type="email"
73-
placeholder="Enter your email"
81+
placeholder={t('enterEmail')}
7482
{...register('email')}
7583
disabled={isLoading}
7684
/>
@@ -80,11 +88,11 @@ export default function SignInPage() {
8088
</div>
8189

8290
<div>
83-
<Label htmlFor="password">Password</Label>
91+
<Label htmlFor="password">{t('password')}</Label>
8492
<Input
8593
id="password"
8694
type="password"
87-
placeholder="Enter your password"
95+
placeholder={t('enterPassword')}
8896
{...register('password')}
8997
disabled={isLoading}
9098
/>
@@ -106,17 +114,17 @@ export default function SignInPage() {
106114
>
107115
{isLoading ? (
108116
<div className="flex items-center justify-center">
109-
<LoadingSpinner size="sm" className="mr-2" />
110-
Signing in...
117+
<LoadingSpinner size="sm" className={locale === 'ar' ? 'ml-2' : 'mr-2'} />
118+
{t('signingIn')}
111119
</div>
112120
) : (
113-
'Sign In'
121+
t('signIn')
114122
)}
115123
</Button>
116124
</form>
117125

118126
<div className="mt-6 text-sm text-gray-600">
119-
<p className="font-semibold">Demo Accounts:</p>
127+
<p className="font-semibold">{t('demoAccounts')}:</p>
120128
<div className="mt-2 space-y-1">
121129
<p><strong>CEO:</strong> ceo@example.com / Passw0rd!</p>
122130
<p><strong>Executive:</strong> exec@example.com / Passw0rd!</p>

app/[locale]/test/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
interface TestPageProps {
2+
params: Promise<{ locale: string }>
3+
}
4+
5+
export default async function TestPage({ params }: TestPageProps) {
6+
const { locale } = await params
7+
8+
return (
9+
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
10+
<h1>Test Page</h1>
11+
<p>Current locale: {locale}</p>
12+
<p>This is a test page to verify locale routing works.</p>
13+
<div style={{ direction: locale === 'ar' ? 'rtl' : 'ltr' }}>
14+
<p>{locale === 'ar' ? 'مرحبا بك في صفحة الاختبار' : 'Welcome to the test page'}</p>
15+
</div>
16+
</div>
17+
)
18+
}

0 commit comments

Comments
 (0)