first commit

This commit is contained in:
2026-06-03 18:31:41 +03:30
commit 01f87cc461
71 changed files with 10802 additions and 0 deletions

76
.gitignore vendored Normal file
View File

@@ -0,0 +1,76 @@
# ========================
# .NET Core
# ========================
bin/
obj/
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
*.userprefs
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# NuGet packages
*.nupkg
**/[Pp]ackages/*
!**/[Pp]ackages/build/
# Rider / VS Code
.idea/
.vscode/
# App settings with secrets (optional but recommended)
appsettings.*.json
!appsettings.json
# EF Core migrations snapshot can be committed, but we ignore the actual database files
*.db
*.db-shm
*.db-wal
# User secrets
secrets.json
# ========================
# Node / React / Vite
# ========================
node_modules/
dist/
.env.local
.env.*.local
# Vite cache
.vite/
# Build output of frontend
frontend/dist/
frontend/node_modules/
frontend/.vite/
# ========================
# OS generated files
# ========================
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
# ========================
# IDE & Editor
# ========================
*.swp
*.swo
*~
*.sublime-workspace
*.sublime-project
*.code-workspace

22
GhadirContest.sln Normal file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "backend", "backend\backend.csproj", "{2ACC88EF-1841-4049-9CF3-BCBC68FB248E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2ACC88EF-1841-4049-9CF3-BCBC68FB248E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2ACC88EF-1841-4049-9CF3-BCBC68FB248E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ACC88EF-1841-4049-9CF3-BCBC68FB248E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ACC88EF-1841-4049-9CF3-BCBC68FB248E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

24
backend/ClientApp/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,16 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>مسابقه بزرگ غدیر</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 1000;
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}

4763
backend/ClientApp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.3.0",
"axios": "^1.17.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.16.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tslib": "^2.8.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^5.4.21"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,47 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from 'sonner';
import { UserProvider, useUser } from '@/context/UserContext';
import LandingPage from '@/pages/LandingPage';
import DashboardPage from '@/pages/DashboardPage';
import AdminPage from '@/pages/AdminPage';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user } = useUser();
if (!user.userId) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
}
function App() {
return (
<BrowserRouter>
<UserProvider>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route path="/admin" element={<AdminPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<Toaster
position="top-center"
toastOptions={{
style: {
direction: 'rtl',
fontFamily: 'inherit',
},
}}
/>
</UserProvider>
</BrowserRouter>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,51 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
{
variants: {
variant: {
default: "bg-gradient-primary text-white hover:opacity-90 shadow-md hover:shadow-lg",
secondary: "bg-gradient-secondary text-white hover:opacity-90 shadow-md hover:shadow-lg",
outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white",
ghost: "text-primary hover:bg-primary/10",
destructive: "bg-error text-white hover:bg-error/90",
},
size: {
default: "h-10 px-5 py-2",
sm: "h-9 rounded-md px-3 text-xs",
lg: "h-12 rounded-lg px-8 text-base",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ 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 }

View File

@@ -0,0 +1,69 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-border bg-surface shadow-card transition-all duration-200 hover:shadow-card-hover",
className
)}
{...props}
/>
)
)
Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
)
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-xl font-semibold leading-none tracking-tight", className)}
{...props}
/>
)
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-text-light", className)}
{...props}
/>
)
)
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
)
)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-5 w-5 shrink-0 rounded-md border-2 border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-white",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,116 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-surface p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-right",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-reverse sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-text-light", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-text-light focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ 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 }

View File

@@ -0,0 +1,41 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-5 w-5 rounded-full border-2 border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,105 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
)
Table.displayName = "Table"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b border-border", className)} {...props} />
)
)
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
)
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t border-border bg-secondary/10 font-medium", className)}
{...props}
/>
)
)
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b border-border transition-colors hover:bg-secondary/5 data-[state=selected]:bg-secondary/10",
className
)}
{...props}
/>
)
)
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-right align-middle font-medium text-text-light [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
)
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
)
)
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-text-light", className)}
{...props}
/>
)
)
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-12 items-center justify-center rounded-lg bg-secondary/10 p-1 text-text-light",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-sm cursor-pointer",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-4 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,53 @@
import { createContext, useContext, useState, type ReactNode } from 'react';
interface UserState {
userId: number | null;
fullName: string;
contact: string;
isQuizCompleted: boolean;
khatmSelections: number[];
totalScore: number;
}
interface UserContextType {
user: UserState;
setUser: (user: Partial<UserState>) => void;
resetUser: () => void;
}
const initialState: UserState = {
userId: null,
fullName: '',
contact: '',
isQuizCompleted: false,
khatmSelections: [],
totalScore: 0,
};
const UserContext = createContext<UserContextType | undefined>(undefined);
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUserState] = useState<UserState>(initialState);
const setUser = (partial: Partial<UserState>) => {
setUserState(prev => ({ ...prev, ...partial }));
};
const resetUser = () => {
setUserState(initialState);
};
return (
<UserContext.Provider value={{ user, setUser, resetUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}

View File

@@ -0,0 +1,78 @@
@import "tailwindcss";
@theme {
--color-primary: #b8860b;
--color-primary-light: #daa520;
--color-primary-dark: #8b6914;
--color-secondary: #2d6a4f;
--color-secondary-light: #40916c;
--color-secondary-dark: #1b4332;
--color-accent: #f59e0b;
--color-background: #fefcf3;
--color-surface: #ffffff;
--color-text: #1a1a2e;
--color-text-light: #6b7280;
--color-border: #e5e7eb;
--color-error: #dc2626;
--color-success: #16a34a;
}
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Vazirmatn', 'Tahoma', system-ui, sans-serif;
background-color: var(--color-background);
color: var(--color-text);
direction: rtl;
min-height: 100vh;
}
#root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
}
@layer utilities {
.text-gradient {
background: linear-gradient(135deg, #b8860b, #daa520, #8b6914);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.bg-gradient-primary {
background: linear-gradient(135deg, #b8860b, #daa520);
}
.bg-gradient-secondary {
background: linear-gradient(135deg, #2d6a4f, #40916c);
}
.shadow-card {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.shadow-card-hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
.animate-bounce-gentle {
animation: bounce-gentle 2s ease-in-out infinite;
}
}
@keyframes bounce-gentle {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}

View File

@@ -0,0 +1,123 @@
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
export interface SiteConfig {
siteTitle: string;
subtitle: string;
channels: { name: string; url: string }[];
shortText: string;
quizParticipationPoints: number;
quizCorrectAnswerPoints: number;
}
export interface RegisterResponse {
userId: number;
trackingCode: string;
}
export interface LoginResponse {
userId: number;
fullName: string;
contact: string;
isQuizCompleted: boolean;
khatmSelections: number[];
}
export interface Question {
id: number;
questionText: string;
options: string[];
}
export interface QuizSubmitResponse {
participationPoints: number;
correctCount: number;
totalQuizPoints: number;
totalScore: number;
}
export interface UserQuizResponse {
questions: UserQuestion[];
participationPoints: number;
correctCount: number;
totalQuizPoints: number;
}
export interface UserQuestion {
id: number;
questionText: string;
options: string[];
selectedOption: number;
isCorrect: boolean;
}
export interface KhatmItem {
id: number;
title: string;
url?: string;
points: number;
}
export interface KhatmSaveResponse {
success: boolean;
totalScore: number;
}
export interface UserScore {
totalScore: number;
}
export interface AdminReportItem {
userId: number;
fullName: string;
contact: string;
trackingCode: string;
hasTakenQuiz: boolean;
participationPoints: number;
correctCount: number;
totalQuizPoints: number;
khatmTitles: string;
totalScore: number;
}
// Config
export const getConfig = () => api.get<SiteConfig>('/config').then(r => r.data);
// User
export const registerUser = (fullName: string, contact: string) =>
api.post<RegisterResponse>('/user/register', { fullName, contact }).then(r => r.data);
export const loginUser = (trackingCode: string) =>
api.post<LoginResponse>('/user/login', { trackingCode }).then(r => r.data);
export const getUserScore = (userId: number) =>
api.get<UserScore>(`/user/${userId}/score`).then(r => r.data);
// Quiz
export const getQuestions = () =>
api.get<Question[]>('/quiz/questions').then(r => r.data);
export const submitQuiz = (userId: number, answers: { questionId: number; selectedOption: number }[]) =>
api.post<QuizSubmitResponse>('/quiz/submit', { userId, answers }).then(r => r.data);
export const getUserQuiz = (userId: number) =>
api.get<UserQuizResponse>(`/quiz/user/${userId}`).then(r => r.data);
// Khatm
export const getKhatmItems = () =>
api.get<KhatmItem[]>('/khatm/items').then(r => r.data);
export const saveKhatmSelections = (userId: number, selections: { khatmItemId: number; isSelected: boolean }[]) =>
api.post<KhatmSaveResponse>('/khatm/save', { userId, selections }).then(r => r.data);
// Admin
export const getAdminReport = (password: string) =>
api.get<AdminReportItem[]>('/admin/report', { params: { password } }).then(r => r.data);
export default api;

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,211 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { Shield, Eye, EyeOff, Download } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { getAdminReport, type AdminReportItem } from '@/lib/api';
export default function AdminPage() {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
const [loading, setLoading] = useState(false);
const [report, setReport] = useState<AdminReportItem[]>([]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!password.trim()) {
toast.error('لطفاً رمز عبور را وارد کنید.');
return;
}
setLoading(true);
try {
const data = await getAdminReport(password);
setReport(data);
setAuthenticated(true);
toast.success('ورود موفقیت‌آمیز');
} catch (err: any) {
if (err.response?.status === 403) {
toast.error('رمز عبور اشتباه است.');
} else {
toast.error('خطا در ارتباط با سرور');
}
} finally {
setLoading(false);
}
};
const exportCSV = () => {
const headers = ['ردیف', 'نام', 'شناسه', 'کد رهگیری', 'شرکت در مسابقه', 'امتیاز شرکت', 'پاسخ صحیح', 'امتیاز مسابقه', 'ختم‌ها', 'مجموع امتیاز'];
const rows = report.map((item, index) => [
index + 1,
item.fullName,
item.contact,
item.trackingCode,
item.hasTakenQuiz ? 'بله' : 'خیر',
item.participationPoints,
item.correctCount,
item.totalQuizPoints,
item.khatmTitles,
item.totalScore,
]);
const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const BOM = '\uFEFF';
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'گزارش-مسابقه-غدیر.csv';
a.click();
URL.revokeObjectURL(url);
toast.success('گزارش با موفقیت دانلود شد.');
};
if (!authenticated) {
return (
<div className="min-h-screen bg-gradient-to-b from-background via-primary/5 to-background flex items-center justify-center p-4">
<Card className="w-full max-w-md border-primary/20">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<div className="p-3 rounded-full bg-primary/10">
<Shield className="h-8 w-8 text-primary" />
</div>
</div>
<CardTitle className="text-2xl text-gradient">پنل مدیریت</CardTitle>
<CardDescription>
برای دسترسی به گزارش کاربران، رمز عبور را وارد کنید
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-4">
<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
placeholder="رمز عبور"
value={password}
onChange={e => setPassword(e.target.value)}
className="pl-10 text-center"
autoFocus
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light hover:text-text transition-colors"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'در حال بررسی...' : 'ورود به پنل مدیریت'}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-b from-background via-primary/5 to-background">
<header className="border-b border-border/50 bg-surface/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<Shield className="h-6 w-6 text-primary" />
<span className="font-semibold text-lg text-gradient">پنل مدیریت</span>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-text-light">
تعداد کاربران: {report.length}
</span>
<Button variant="outline" size="sm" onClick={exportCSV} className="gap-2">
<Download className="h-4 w-4" />
خروجی CSV
</Button>
<Button variant="ghost" size="sm" onClick={() => setAuthenticated(false)}>
خروج
</Button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-gradient">گزارش کامل کاربران</CardTitle>
<CardDescription>
اطلاعات تمام کاربران به همراه وضعیت مسابقه و ختم‌ها
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>ردیف</TableHead>
<TableHead>نام</TableHead>
<TableHead>شناسه</TableHead>
<TableHead>کد رهگیری</TableHead>
<TableHead>مسابقه</TableHead>
<TableHead>امتیاز شرکت</TableHead>
<TableHead>پاسخ صحیح</TableHead>
<TableHead>امتیاز مسابقه</TableHead>
<TableHead>ختم‌ها</TableHead>
<TableHead>مجموع امتیاز</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{report.map((item, index) => (
<TableRow key={item.userId}>
<TableCell className="font-medium">{index + 1}</TableCell>
<TableCell>{item.fullName}</TableCell>
<TableCell dir="ltr" className="text-left">{item.contact}</TableCell>
<TableCell>
<span className="font-mono text-xs bg-primary/5 px-2 py-1 rounded" dir="ltr">
{item.trackingCode}
</span>
</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
item.hasTakenQuiz
? 'bg-success/10 text-success'
: 'bg-error/10 text-error'
}`}>
{item.hasTakenQuiz ? 'شرکت کرده' : 'شرکت نکرده'}
</span>
</TableCell>
<TableCell>{item.participationPoints}</TableCell>
<TableCell>{item.correctCount}</TableCell>
<TableCell className="font-medium">{item.totalQuizPoints}</TableCell>
<TableCell className="max-w-[200px] truncate" title={item.khatmTitles}>
{item.khatmTitles || '—'}
</TableCell>
<TableCell className="font-bold text-primary">{item.totalScore}</TableCell>
</TableRow>
))}
{report.length === 0 && (
<TableRow>
<TableCell colSpan={10} className="text-center py-8 text-text-light">
هیچ کاربری یافت نشد.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</main>
</div>
);
}

View File

@@ -0,0 +1,481 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { Award, BookOpen, CheckCircle, ClipboardList, ExternalLink, LogOut, Trophy, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import {
getQuestions,
submitQuiz,
getUserQuiz,
getKhatmItems,
saveKhatmSelections,
getUserScore,
type Question,
type UserQuizResponse,
type KhatmItem,
} from '@/lib/api';
import { useUser } from '@/context/UserContext';
export default function DashboardPage() {
const navigate = useNavigate();
const { user, setUser, resetUser } = useUser();
// Quiz state
const [questions, setQuestions] = useState<Question[]>([]);
const [userQuiz, setUserQuiz] = useState<UserQuizResponse | null>(null);
const [answers, setAnswers] = useState<Record<number, number>>({});
const [quizLoading, setQuizLoading] = useState(false);
const [showQuizDialog, setShowQuizDialog] = useState(false);
const [quizResult, setQuizResult] = useState<{
participationPoints: number;
correctCount: number;
totalQuizPoints: number;
} | null>(null);
const [submittingQuiz, setSubmittingQuiz] = useState(false);
// Khatm state
const [khatmItems, setKhatmItems] = useState<KhatmItem[]>([]);
const [khatmSelections, setKhatmSelections] = useState<Record<number, boolean>>({});
const [khatmLoading, setKhatmLoading] = useState(false);
const [savingKhatm, setSavingKhatm] = useState(false);
// Score
const [totalScore, setTotalScore] = useState(0);
// Section refs for scroll navigation
const quizSectionRef = useRef<HTMLDivElement>(null);
const khatmSectionRef = useRef<HTMLDivElement>(null);
const scrollToSection = (ref: React.RefObject<HTMLDivElement | null>) => {
if (ref.current) {
const headerOffset = 140; // account for sticky header + nav
const elementPosition = ref.current.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - headerOffset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
useEffect(() => {
if (!user.userId) {
navigate('/');
return;
}
loadDashboard();
}, [user.userId]);
const loadDashboard = async () => {
setQuizLoading(true);
setKhatmLoading(true);
try {
// Load score
const scoreData = await getUserScore(user.userId!);
setTotalScore(scoreData.totalScore);
setUser({ totalScore: scoreData.totalScore });
// Load khatm items
const items = await getKhatmItems();
setKhatmItems(items);
// Initialize selections from user context
const selections: Record<number, boolean> = {};
items.forEach(item => {
selections[item.id] = user.khatmSelections.includes(item.id);
});
setKhatmSelections(selections);
// Load quiz data
if (user.isQuizCompleted) {
const quizData = await getUserQuiz(user.userId!);
setUserQuiz(quizData);
} else {
const qs = await getQuestions();
setQuestions(qs);
}
} catch (err: any) {
toast.error('خطا در بارگذاری اطلاعات');
} finally {
setQuizLoading(false);
setKhatmLoading(false);
}
};
const handleStartQuiz = () => {
setShowQuizDialog(true);
};
const handleSubmitQuiz = async () => {
const answeredCount = Object.keys(answers).length;
if (answeredCount < questions.length) {
toast.error(`لطفاً به تمام ${questions.length} سوال پاسخ دهید.`);
return;
}
setSubmittingQuiz(true);
try {
const result = await submitQuiz(
user.userId!,
Object.entries(answers).map(([questionId, selectedOption]) => ({
questionId: Number(questionId),
selectedOption,
}))
);
setQuizResult({
participationPoints: result.participationPoints,
correctCount: result.correctCount,
totalQuizPoints: result.totalQuizPoints,
});
setTotalScore(result.totalScore);
setUser({ isQuizCompleted: true, totalScore: result.totalScore });
// Load the completed quiz
const quizData = await getUserQuiz(user.userId!);
setUserQuiz(quizData);
} catch (err: any) {
toast.error(err.response?.data?.message || 'خطا در ثبت پاسخ‌ها');
} finally {
setSubmittingQuiz(false);
}
};
const handleSaveKhatm = async () => {
setSavingKhatm(true);
try {
const selections = Object.entries(khatmSelections).map(([khatmItemId, isSelected]) => ({
khatmItemId: Number(khatmItemId),
isSelected,
}));
const result = await saveKhatmSelections(user.userId!, selections);
setTotalScore(result.totalScore);
setUser({ totalScore: result.totalScore, khatmSelections: Object.entries(khatmSelections).filter(([, v]) => v).map(([k]) => Number(k)) });
toast.success('تغییرات با موفقیت ذخیره شد.');
} catch (err: any) {
toast.error('خطا در ذخیره تغییرات');
} finally {
setSavingKhatm(false);
}
};
const handleLogout = () => {
resetUser();
navigate('/');
};
if (!user.userId) return null;
return (
<div className="min-h-screen bg-gradient-to-b from-background via-primary/5 to-background">
{/* Header */}
<header className="border-b border-border/50 bg-surface/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<Trophy className="h-6 w-6 text-primary" />
<span className="font-semibold text-lg text-gradient">داشبورد</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 bg-primary/10 rounded-full px-4 py-1.5">
<Award className="h-5 w-5 text-primary" />
<span className="font-bold text-primary">{totalScore}</span>
<span className="text-xs text-text-light">امتیاز</span>
</div>
<Button variant="ghost" size="sm" onClick={handleLogout} className="gap-2">
<LogOut className="h-4 w-4" />
خروج
</Button>
</div>
</div>
</header>
{/* Section Navigation */}
<nav className="sticky top-16 z-30 bg-surface/90 backdrop-blur-sm border-b border-border/50">
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-center gap-3">
<Button
variant="outline"
size="sm"
onClick={() => scrollToSection(quizSectionRef)}
className="gap-2.5 text-sm font-medium rounded-xl border-primary/20 bg-primary/5 hover:bg-primary/10 hover:border-primary/40 transition-all duration-200 shadow-sm px-5 py-2.5"
>
<BookOpen className="h-4 w-4 text-primary" />
<span className="text-primary">مسابقه</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => scrollToSection(khatmSectionRef)}
className="gap-2.5 text-sm font-medium rounded-xl border-secondary/20 bg-secondary/5 hover:bg-secondary/10 hover:border-secondary/40 transition-all duration-200 shadow-sm px-5 py-2.5"
>
<ClipboardList className="h-4 w-4 text-secondary" />
<span className="text-secondary">ختم</span>
</Button>
</div>
</nav>
<main className="max-w-6xl mx-auto px-4 py-8">
{/* Welcome */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-gradient mb-1">
{user.fullName} خوش آمدی،
</h2>
<p className="text-text-light">امتیاز کل شما: {totalScore}</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Quiz Card */}
<div ref={quizSectionRef}>
<Card className="border-primary/20 scroll-mt-36">
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<BookOpen className="h-6 w-6 text-primary" />
</div>
<div>
<CardTitle>مسابقه</CardTitle>
<CardDescription>
{user.isQuizCompleted
? 'شما قبلاً در مسابقه شرکت کرده‌اید'
: 'به سوالات چهارگزینه‌ای پاسخ دهید'}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{quizLoading ? (
<div className="animate-pulse h-20 bg-secondary/10 rounded-lg" />
) : user.isQuizCompleted && userQuiz ? (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="bg-primary/5 rounded-lg p-3 text-center">
<p className="text-xs text-text-light">امتیاز شرکت</p>
<p className="text-lg font-bold text-primary">{userQuiz.participationPoints}</p>
</div>
<div className="bg-success/5 rounded-lg p-3 text-center">
<p className="text-xs text-text-light">پاسخ صحیح</p>
<p className="text-lg font-bold text-success">{userQuiz.correctCount}</p>
</div>
<div className="bg-secondary/5 rounded-lg p-3 text-center">
<p className="text-xs text-text-light">امتیاز کل</p>
<p className="text-lg font-bold text-secondary">{userQuiz.totalQuizPoints}</p>
</div>
</div>
<div className="space-y-4">
{userQuiz.questions.map((q, index) => (
<div key={q.id} className="border border-border rounded-lg p-4">
<p className="font-medium mb-3">
{index + 1}. {q.questionText}
</p>
<div className="space-y-2">
{q.options.map((opt, optIndex) => (
<div
key={optIndex}
className={`flex items-center gap-2 p-2 rounded-md ${
q.selectedOption === optIndex + 1
? q.isCorrect
? 'bg-success/10 text-success'
: 'bg-error/10 text-error'
: ''
}`}
>
<span className="text-sm flex-1">{opt}</span>
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 ${
q.selectedOption === optIndex + 1
? q.isCorrect
? 'border-success'
: 'border-error'
: 'border-border'
}`}>
{q.selectedOption === optIndex + 1 && (
<div className={`w-2.5 h-2.5 rounded-full ${
q.isCorrect ? 'bg-success' : 'bg-error'
}`} />
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
) : (
<div className="text-center py-6">
<Sparkles className="h-12 w-12 text-primary/40 mx-auto mb-3" />
<p className="text-text-light mb-4">
آماده شرکت در مسابقه هستید؟ {questions.length} سوال چهارگزینه‌ای منتظر شماست.
</p>
<Button onClick={handleStartQuiz} className="gap-2">
<CheckCircle className="h-4 w-4" />
شروع مسابقه
</Button>
</div>
)}
</CardContent>
</Card>
</div>
{/* Khatm Card */}
<div ref={khatmSectionRef}>
<Card className="border-secondary/20 scroll-mt-36">
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-secondary/10">
<ClipboardList className="h-6 w-6 text-secondary" />
</div>
<div>
<CardTitle>ثبت ختم‌ها</CardTitle>
<CardDescription>
ختم‌های مورد نظر خود را انتخاب کنید
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{khatmLoading ? (
<div className="animate-pulse space-y-3">
{[1, 2, 3].map(i => (
<div key={i} className="h-12 bg-secondary/10 rounded-lg" />
))}
</div>
) : (
<div className="space-y-3">
{khatmItems.map(item => (
<div
key={item.id}
className={`flex items-center justify-between p-3 rounded-lg border transition-all duration-200 ${
khatmSelections[item.id]
? 'border-secondary/50 bg-secondary/5'
: 'border-border hover:border-secondary/30'
}`}
>
<div className="flex items-center gap-3">
<Checkbox
id={`khatm-${item.id}`}
checked={khatmSelections[item.id] || false}
onCheckedChange={(checked) => {
setKhatmSelections(prev => ({
...prev,
[item.id]: checked === true,
}));
}}
/>
<Label htmlFor={`khatm-${item.id}`} className="cursor-pointer text-sm font-medium">
{item.title}
</Label>
{item.url && (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-text-light hover:text-primary transition-colors"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
</div>
<span className="text-xs bg-secondary/10 text-secondary px-2 py-1 rounded-full font-medium">
{item.points} امتیاز
</span>
</div>
))}
<Button
onClick={handleSaveKhatm}
variant="secondary"
className="w-full mt-4"
disabled={savingKhatm}
>
{savingKhatm ? 'در حال ذخیره...' : 'ثبت تغییرات'}
</Button>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</main>
{/* Quiz Dialog */}
<Dialog open={showQuizDialog} onOpenChange={(open) => {
if (!open && !quizResult) setShowQuizDialog(false);
}}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
{quizResult ? (
<>
<DialogHeader>
<DialogTitle className="text-center text-2xl text-gradient">نتیجه مسابقه</DialogTitle>
<DialogDescription className="text-center">
پاسخ‌های شما با موفقیت ثبت شد
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-3 gap-4 py-4">
<div className="bg-primary/5 rounded-xl p-4 text-center">
<p className="text-xs text-text-light mb-1">امتیاز شرکت</p>
<p className="text-2xl font-bold text-primary">{quizResult.participationPoints}</p>
</div>
<div className="bg-success/5 rounded-xl p-4 text-center">
<p className="text-xs text-text-light mb-1">پاسخ‌های صحیح</p>
<p className="text-2xl font-bold text-success">{quizResult.correctCount}</p>
</div>
<div className="bg-secondary/5 rounded-xl p-4 text-center">
<p className="text-xs text-text-light mb-1">امتیاز مسابقه</p>
<p className="text-2xl font-bold text-secondary">{quizResult.totalQuizPoints}</p>
</div>
</div>
<Button onClick={() => { setShowQuizDialog(false); setQuizResult(null); }} className="w-full">
بازگشت به داشبورد
</Button>
</>
) : (
<>
<DialogHeader>
<DialogTitle className="text-xl text-gradient">سوالات مسابقه</DialogTitle>
<DialogDescription>
به تمام سوالات پاسخ دهید و سپس دکمه ثبت را بزنید
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{questions.map((q, index) => (
<div key={q.id} className="border border-border rounded-lg p-4">
<p className="font-medium mb-3">
{index + 1}. {q.questionText}
</p>
<RadioGroup
value={String(answers[q.id] || '')}
onValueChange={(value) => {
setAnswers(prev => ({ ...prev, [q.id]: Number(value) }));
}}
>
{q.options.map((opt, optIndex) => (
<div key={optIndex} className="flex items-center gap-2 p-2 rounded-md hover:bg-secondary/5">
<Label htmlFor={`q${q.id}-o${optIndex}`} className="cursor-pointer text-sm flex-1 order-1">
{opt}
</Label>
<RadioGroupItem value={String(optIndex + 1)} id={`q${q.id}-o${optIndex}`} className="order-2" />
</div>
))}
</RadioGroup>
</div>
))}
</div>
<Button
onClick={handleSubmitQuiz}
className="w-full"
disabled={submittingQuiz}
>
{submittingQuiz ? 'در حال ثبت...' : 'ثبت پاسخ‌ها'}
</Button>
</>
)}
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,298 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Copy, Check, UserPlus, LogIn, ExternalLink, Award, Gamepad2 } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { getConfig, registerUser, loginUser, type SiteConfig } from '@/lib/api';
import { useUser } from '@/context/UserContext';
export default function LandingPage() {
const navigate = useNavigate();
const { setUser } = useUser();
const [config, setConfig] = useState<SiteConfig | null>(null);
const [loading, setLoading] = useState(true);
const [showAuthDialog, setShowAuthDialog] = useState(false);
// Register form
const [regName, setRegName] = useState('');
const [regContact, setRegContact] = useState('');
const [regSubmitting, setRegSubmitting] = useState(false);
// Login form
const [loginCode, setLoginCode] = useState('');
const [loginSubmitting, setLoginSubmitting] = useState(false);
// Tracking code dialog
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
const [trackingCode, setTrackingCode] = useState('');
const [copied, setCopied] = useState(false);
// Active tab
const [activeTab, setActiveTab] = useState('register');
useEffect(() => {
getConfig()
.then(setConfig)
.catch(() => toast.error('خطا در دریافت اطلاعات سایت'))
.finally(() => setLoading(false));
}, []);
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
if (!regName.trim() || !regContact.trim()) {
toast.error('لطفاً نام و شناسه خود را وارد کنید.');
return;
}
if (regContact.trim().length < 5) {
toast.error('شناسه باید حداقل ۵ کاراکتر باشد.');
return;
}
setRegSubmitting(true);
try {
const result = await registerUser(regName.trim(), regContact.trim());
setTrackingCode(result.trackingCode);
setShowTrackingDialog(true);
setUser({ userId: result.userId, fullName: regName.trim(), contact: regContact.trim() });
} catch (err: any) {
if (err.response?.status === 409) {
toast.error('این شناسه قبلاً ثبت شده است. لطفاً با کد رهگیری وارد شوید.');
setActiveTab('login');
} else {
toast.error(err.response?.data?.message || 'خطا در ثبت‌نام');
}
} finally {
setRegSubmitting(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!loginCode.trim()) {
toast.error('لطفاً کد رهگیری را وارد کنید.');
return;
}
setLoginSubmitting(true);
try {
const result = await loginUser(loginCode.trim());
setUser({
userId: result.userId,
fullName: result.fullName,
contact: result.contact,
isQuizCompleted: result.isQuizCompleted,
khatmSelections: result.khatmSelections,
});
navigate('/dashboard');
} catch (err: any) {
if (err.response?.status === 404) {
toast.error('کد رهگیری نامعتبر است.');
} else {
toast.error('خطا در ورود');
}
} finally {
setLoginSubmitting(false);
}
};
const copyTrackingCode = () => {
navigator.clipboard.writeText(trackingCode);
setCopied(true);
toast.success('کد رهگیری کپی شد.');
setTimeout(() => setCopied(false), 2000);
};
const goToDashboard = () => {
setShowTrackingDialog(false);
navigate('/dashboard');
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-pulse text-primary text-lg">در حال بارگذاری...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-b from-background via-primary/5 to-background">
{/* Hero Section */}
<div className="relative overflow-hidden">
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiM5OTc1M2QiIGZpbGwtb3BhY2l0eT0iMC4wNSI+PHBhdGggZD0iTTM2IDM0djItSDI0di0yaDEyek0zNiAyNHYySDI0di0yaDEyeiIvPjwvZz48L2c+PC9zdmc+')] opacity-30" />
<div className="relative max-w-4xl mx-auto px-4 py-12 md:py-20">
{/* Decorative top border */}
<div className="flex justify-center mb-8">
<div className="h-1 w-32 bg-gradient-primary rounded-full" />
</div>
{/* Title */}
<h1 className="text-4xl md:text-6xl font-bold text-center mb-4">
<span className="text-gradient">{config?.siteTitle || 'مسابقه بزرگ غدیر'}</span>
</h1>
{/* Subtitle */}
<p className="text-lg md:text-xl text-center text-text-light mb-8">
{config?.subtitle}
</p>
{/* Channel Links */}
{config?.channels && (
<div className="flex flex-wrap justify-center gap-4 mb-8">
{config.channels.map((channel, index) => (
<a
key={index}
href={channel.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-gradient-secondary text-white hover:opacity-90 transition-all duration-200 shadow-md hover:shadow-lg text-sm font-medium"
>
<ExternalLink className="h-4 w-4" />
{channel.name}
</a>
))}
</div>
)}
{/* Short Text */}
{config?.shortText && (
<p className="text-center text-text-light max-w-2xl mx-auto mb-10 leading-relaxed">
{config.shortText}
</p>
)}
{/* Decorative divider */}
<div className="flex items-center justify-center gap-3 mb-10">
<div className="h-px w-16 bg-gradient-primary" />
<Award className="h-6 w-6 text-primary" />
<div className="h-px w-16 bg-gradient-primary" />
</div>
{/* CTA Button - Enter Contest */}
<div className="flex justify-center">
<Button
onClick={() => setShowAuthDialog(true)}
size="lg"
className="text-lg px-10 py-6 rounded-2xl shadow-2xl hover:shadow-primary/25 animate-bounce-gentle"
>
<Gamepad2 className="h-6 w-6 ml-2" />
ورود به مسابقه
</Button>
</div>
</div>
</div>
{/* Auth Dialog */}
<Dialog open={showAuthDialog} onOpenChange={setShowAuthDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader className="text-center pb-2">
<DialogTitle className="text-2xl text-gradient">ورود به مسابقه</DialogTitle>
<DialogDescription>
برای شرکت در مسابقه، ثبت‌نام کنید یا با کد رهگیری وارد شوید
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="register" className="flex items-center gap-2">
<UserPlus className="h-4 w-4" />
ثبت‌نام جدید
</TabsTrigger>
<TabsTrigger value="login" className="flex items-center gap-2">
<LogIn className="h-4 w-4" />
ورود با کد رهگیری
</TabsTrigger>
</TabsList>
<TabsContent value="register">
<form onSubmit={handleRegister} className="space-y-4">
<div className="space-y-2 text-center">
<Label htmlFor="regName" className="block">نام و نام‌خانوادگی</Label>
<Input
id="regName"
placeholder="مثال: علی محمدی"
value={regName}
onChange={e => setRegName(e.target.value)}
required
className="text-center"
dir="rtl"
/>
</div>
<div className="space-y-2 text-center">
<Label htmlFor="regContact" className="block">شناسه (شماره موبایل یا ID ایتا)</Label>
<Input
id="regContact"
placeholder="مثال: 09123456789"
value={regContact}
onChange={e => setRegContact(e.target.value)}
required
minLength={5}
className="text-center"
dir="ltr"
/>
</div>
<Button type="submit" className="w-full" disabled={regSubmitting}>
{regSubmitting ? 'در حال ثبت‌نام...' : 'شرکت در مسابقه'}
</Button>
</form>
</TabsContent>
<TabsContent value="login">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2 text-center">
<Label htmlFor="loginCode" className="block">کد رهگیری</Label>
<Input
id="loginCode"
placeholder="کد ۴ رقمی خود را وارد کنید"
value={loginCode}
onChange={e => setLoginCode(e.target.value)}
required
maxLength={4}
className="text-center font-mono text-lg tracking-widest"
dir="ltr"
/>
</div>
<Button type="submit" className="w-full" variant="secondary" disabled={loginSubmitting}>
{loginSubmitting ? 'در حال ورود...' : 'ورود'}
</Button>
</form>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
{/* Tracking Code Dialog */}
<Dialog open={showTrackingDialog} onOpenChange={setShowTrackingDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-center text-2xl text-gradient">ثبت‌نام با موفقیت انجام شد!</DialogTitle>
<DialogDescription className="text-center">
کد رهگیری خود را حتماً یادداشت کنید. این تنها راه دسترسی مجدد به حساب شماست.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center gap-4 py-4">
<div className="bg-primary/5 border-2 border-primary/30 rounded-xl p-6 text-center w-full">
<p className="text-sm text-text-light mb-2">کد رهگیری شما:</p>
<p className="text-3xl font-bold tracking-[0.3em] text-primary font-mono" dir="ltr">
{trackingCode}
</p>
</div>
<Button onClick={copyTrackingCode} variant="outline" className="w-full gap-2">
{copied ? (
<><Check className="h-4 w-4 text-success" /> کپی شد</>
) : (
<><Copy className="h-4 w-4" /> کپی کد رهگیری</>
)}
</Button>
<Button onClick={goToDashboard} className="w-full">
ورود به داشبورد
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,27 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
},
})

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using backend.Data;
using backend.DTOs;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
private readonly AppDbContext _db;
private readonly AppConfig _appConfig;
public AdminController(AppDbContext db, IOptions<AppConfig> appConfig)
{
_db = db;
_appConfig = appConfig.Value;
}
[HttpGet("report")]
public async Task<IActionResult> GetReport([FromQuery] string password)
{
if (password != _appConfig.AdminPassword)
{
return StatusCode(403, new { message = "رمز عبور اشتباه است." });
}
var users = await _db.Users
.Include(u => u.QuizAttempt)
.Include(u => u.KhatmSelections)
.ThenInclude(ks => ks.KhatmItem)
.Include(u => u.TotalScore)
.OrderBy(u => u.Id)
.ToListAsync();
var report = users.Select(u => new AdminReportItem
{
UserId = u.Id,
FullName = u.FullName,
Contact = u.Contact,
TrackingCode = u.TrackingCode,
HasTakenQuiz = u.QuizAttempt != null,
ParticipationPoints = u.QuizAttempt?.ParticipationPoints ?? 0,
CorrectCount = u.QuizAttempt?.CorrectAnswersCount ?? 0,
TotalQuizPoints = u.QuizAttempt?.TotalQuizPoints ?? 0,
KhatmTitles = string.Join("، ",
u.KhatmSelections
.Where(ks => ks.IsSelected)
.Select(ks => ks.KhatmItem.Title)),
TotalScore = u.TotalScore?.TotalPoints ?? 0
}).ToList();
return Ok(report);
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
private readonly AppConfig _appConfig;
public ConfigController(IOptions<AppConfig> appConfig)
{
_appConfig = appConfig.Value;
}
[HttpGet]
public IActionResult GetConfig()
{
return Ok(new
{
siteTitle = _appConfig.SiteTitle,
subtitle = _appConfig.Subtitle,
channels = _appConfig.Channels.Select(c => new { name = c.Name, url = c.Url }),
shortText = _appConfig.ShortText,
quizParticipationPoints = _appConfig.Quiz.ParticipationPoints,
quizCorrectAnswerPoints = _appConfig.Quiz.CorrectAnswerPoints
});
}
}

View File

@@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using backend.Data;
using backend.DTOs;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class KhatmController : ControllerBase
{
private readonly AppDbContext _db;
public KhatmController(AppDbContext db)
{
_db = db;
}
[HttpGet("items")]
public async Task<IActionResult> GetItems()
{
var items = await _db.KhatmItems
.OrderBy(k => k.Id)
.Select(k => new KhatmItemDto
{
Id = k.Id,
Title = k.Title,
Url = k.Url,
Points = k.Points
})
.ToListAsync();
return Ok(items);
}
[HttpPost("save")]
public async Task<IActionResult> SaveSelections([FromBody] KhatmSaveRequest request)
{
var user = await _db.Users.FindAsync(request.UserId);
if (user == null)
{
return NotFound(new { message = "کاربر یافت نشد." });
}
foreach (var selection in request.Selections)
{
var existing = await _db.UserKhatmSelections
.FirstOrDefaultAsync(ks => ks.UserId == request.UserId && ks.KhatmItemId == selection.KhatmItemId);
if (existing != null)
{
existing.IsSelected = selection.IsSelected;
existing.LastModified = DateTime.UtcNow;
}
else
{
_db.UserKhatmSelections.Add(new UserKhatmSelection
{
UserId = request.UserId,
KhatmItemId = selection.KhatmItemId,
IsSelected = selection.IsSelected,
LastModified = DateTime.UtcNow
});
}
}
// Recalculate total score
await RecalculateTotalScore(request.UserId);
await _db.SaveChangesAsync();
var totalScore = await _db.UserTotalScores
.Where(ts => ts.UserId == request.UserId)
.Select(ts => ts.TotalPoints)
.FirstOrDefaultAsync();
return Ok(new KhatmSaveResponse
{
Success = true,
TotalScore = totalScore
});
}
private async Task RecalculateTotalScore(int userId)
{
var quizPoints = await _db.UserQuizAttempts
.Where(qa => qa.UserId == userId)
.Select(qa => (int?)qa.TotalQuizPoints)
.FirstOrDefaultAsync() ?? 0;
var khatmPoints = await _db.UserKhatmSelections
.Where(ks => ks.UserId == userId && ks.IsSelected)
.SumAsync(ks => ks.KhatmItem.Points);
int total = quizPoints + khatmPoints;
var totalScore = await _db.UserTotalScores
.FirstOrDefaultAsync(ts => ts.UserId == userId);
if (totalScore != null)
{
totalScore.TotalPoints = total;
}
}
}

View File

@@ -0,0 +1,165 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using backend.Data;
using backend.DTOs;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class QuizController : ControllerBase
{
private readonly AppDbContext _db;
private readonly AppConfig _appConfig;
public QuizController(AppDbContext db, IOptions<AppConfig> appConfig)
{
_db = db;
_appConfig = appConfig.Value;
}
[HttpGet("questions")]
public async Task<IActionResult> GetQuestions()
{
var questions = await _db.QuizQuestions
.OrderBy(q => q.Id)
.Select(q => new QuestionDto
{
Id = q.Id,
QuestionText = q.QuestionText,
Options = new List<string> { q.Option1, q.Option2, q.Option3, q.Option4 }
})
.ToListAsync();
return Ok(questions);
}
[HttpPost("submit")]
public async Task<IActionResult> SubmitQuiz([FromBody] QuizSubmitRequest request)
{
// Check if user already attempted
var existingAttempt = await _db.UserQuizAttempts
.AnyAsync(qa => qa.UserId == request.UserId);
if (existingAttempt)
{
return BadRequest(new { message = "شما قبلاً در مسابقه شرکت کرده‌اید." });
}
var user = await _db.Users.FindAsync(request.UserId);
if (user == null)
{
return NotFound(new { message = "کاربر یافت نشد." });
}
var questions = await _db.QuizQuestions.ToListAsync();
var participationPoints = _appConfig.Quiz.ParticipationPoints;
var correctAnswerPoints = _appConfig.Quiz.CorrectAnswerPoints;
int correctCount = 0;
var answers = new List<UserQuizAnswer>();
foreach (var answer in request.Answers)
{
var question = questions.FirstOrDefault(q => q.Id == answer.QuestionId);
if (question == null) continue;
bool isCorrect = question.CorrectOption == answer.SelectedOption;
if (isCorrect) correctCount++;
answers.Add(new UserQuizAnswer
{
QuestionId = answer.QuestionId,
SelectedOption = answer.SelectedOption,
IsCorrect = isCorrect
});
}
int totalQuizPoints = participationPoints + (correctCount * correctAnswerPoints);
var attempt = new UserQuizAttempt
{
UserId = request.UserId,
ParticipationPoints = participationPoints,
CorrectAnswersCount = correctCount,
TotalQuizPoints = totalQuizPoints,
CompletedAt = DateTime.UtcNow,
Answers = answers
};
_db.UserQuizAttempts.Add(attempt);
// Update total score
await RecalculateTotalScore(request.UserId);
await _db.SaveChangesAsync();
var totalScore = await _db.UserTotalScores
.Where(ts => ts.UserId == request.UserId)
.Select(ts => ts.TotalPoints)
.FirstOrDefaultAsync();
return Ok(new QuizSubmitResponse
{
ParticipationPoints = participationPoints,
CorrectCount = correctCount,
TotalQuizPoints = totalQuizPoints,
TotalScore = totalScore
});
}
[HttpGet("user/{userId}")]
public async Task<IActionResult> GetUserQuiz(int userId)
{
var attempt = await _db.UserQuizAttempts
.Include(qa => qa.Answers)
.ThenInclude(a => a.Question)
.FirstOrDefaultAsync(qa => qa.UserId == userId);
if (attempt == null)
{
return NotFound(new { message = "کاربر هنوز در مسابقه شرکت نکرده است." });
}
var questions = attempt.Answers.Select(a => new UserQuestionDto
{
Id = a.Question.Id,
QuestionText = a.Question.QuestionText,
Options = new List<string> { a.Question.Option1, a.Question.Option2, a.Question.Option3, a.Question.Option4 },
SelectedOption = a.SelectedOption,
IsCorrect = a.IsCorrect
}).ToList();
return Ok(new UserQuizResponse
{
Questions = questions,
ParticipationPoints = attempt.ParticipationPoints,
CorrectCount = attempt.CorrectAnswersCount,
TotalQuizPoints = attempt.TotalQuizPoints
});
}
private async Task RecalculateTotalScore(int userId)
{
var quizPoints = await _db.UserQuizAttempts
.Where(qa => qa.UserId == userId)
.Select(qa => (int?)qa.TotalQuizPoints)
.FirstOrDefaultAsync() ?? 0;
var khatmPoints = await _db.UserKhatmSelections
.Where(ks => ks.UserId == userId && ks.IsSelected)
.SumAsync(ks => ks.KhatmItem.Points);
int total = quizPoints + khatmPoints;
var totalScore = await _db.UserTotalScores
.FirstOrDefaultAsync(ts => ts.UserId == userId);
if (totalScore != null)
{
totalScore.TotalPoints = total;
}
}
}

View File

@@ -0,0 +1,129 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using backend.Data;
using backend.DTOs;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly AppDbContext _db;
public UserController(AppDbContext db)
{
_db = db;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
if (string.IsNullOrWhiteSpace(request.FullName) || string.IsNullOrWhiteSpace(request.Contact))
{
return BadRequest(new { message = "نام و شناسه الزامی هستند." });
}
if (request.Contact.Length < 5)
{
return BadRequest(new { message = "شناسه باید حداقل ۵ کاراکتر باشد." });
}
// Check if contact already exists
var existingUser = await _db.Users.FirstOrDefaultAsync(u => u.Contact == request.Contact);
if (existingUser != null)
{
return Conflict(new { message = "این شناسه قبلاً ثبت شده است. لطفاً با کد رهگیری وارد شوید." });
}
// Generate unique tracking code
string trackingCode;
do
{
trackingCode = GenerateTrackingCode();
} while (await _db.Users.AnyAsync(u => u.TrackingCode == trackingCode));
var user = new User
{
FullName = request.FullName.Trim(),
Contact = request.Contact.Trim(),
TrackingCode = trackingCode
};
_db.Users.Add(user);
// Save the user first so that user.Id is populated by the database
await _db.SaveChangesAsync();
// Create initial total score record with the now-populated user.Id
var totalScore = new UserTotalScore
{
UserId = user.Id,
TotalPoints = 0
};
_db.UserTotalScores.Add(totalScore);
await _db.SaveChangesAsync();
return Ok(new RegisterResponse
{
UserId = user.Id,
TrackingCode = trackingCode
});
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (string.IsNullOrWhiteSpace(request.TrackingCode))
{
return BadRequest(new { message = "کد رهگیری الزامی است." });
}
var user = await _db.Users
.Include(u => u.QuizAttempt)
.Include(u => u.KhatmSelections)
.FirstOrDefaultAsync(u => u.TrackingCode == request.TrackingCode.Trim());
if (user == null)
{
return NotFound(new { message = "کد رهگیری نامعتبر است." });
}
return Ok(new LoginResponse
{
UserId = user.Id,
FullName = user.FullName,
Contact = user.Contact,
IsQuizCompleted = user.QuizAttempt != null,
KhatmSelections = user.KhatmSelections
.Where(ks => ks.IsSelected)
.Select(ks => ks.KhatmItemId)
.ToList()
});
}
[HttpGet("{userId}/score")]
public async Task<IActionResult> GetScore(int userId)
{
var totalScore = await _db.UserTotalScores
.FirstOrDefaultAsync(ts => ts.UserId == userId);
if (totalScore == null)
{
return NotFound(new { message = "کاربر یافت نشد." });
}
return Ok(new UserScoreResponse
{
TotalScore = totalScore.TotalPoints
});
}
private static string GenerateTrackingCode()
{
var random = new Random();
return random.Next(1000, 10000).ToString();
}
}

15
backend/DTOs/AdminDtos.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace backend.DTOs;
public class AdminReportItem
{
public int UserId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Contact { get; set; } = string.Empty;
public string TrackingCode { get; set; } = string.Empty;
public bool HasTakenQuiz { get; set; }
public int ParticipationPoints { get; set; }
public int CorrectCount { get; set; }
public int TotalQuizPoints { get; set; }
public string KhatmTitles { get; set; } = string.Empty;
public int TotalScore { get; set; }
}

27
backend/DTOs/KhatmDtos.cs Normal file
View File

@@ -0,0 +1,27 @@
namespace backend.DTOs;
public class KhatmItemDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Url { get; set; }
public int Points { get; set; }
}
public class KhatmSaveRequest
{
public int UserId { get; set; }
public List<KhatmSelectionDto> Selections { get; set; } = new();
}
public class KhatmSelectionDto
{
public int KhatmItemId { get; set; }
public bool IsSelected { get; set; }
}
public class KhatmSaveResponse
{
public bool Success { get; set; }
public int TotalScore { get; set; }
}

45
backend/DTOs/QuizDtos.cs Normal file
View File

@@ -0,0 +1,45 @@
namespace backend.DTOs;
public class QuestionDto
{
public int Id { get; set; }
public string QuestionText { get; set; } = string.Empty;
public List<string> Options { get; set; } = new();
}
public class QuizSubmitRequest
{
public int UserId { get; set; }
public List<AnswerDto> Answers { get; set; } = new();
}
public class AnswerDto
{
public int QuestionId { get; set; }
public int SelectedOption { get; set; }
}
public class QuizSubmitResponse
{
public int ParticipationPoints { get; set; }
public int CorrectCount { get; set; }
public int TotalQuizPoints { get; set; }
public int TotalScore { get; set; }
}
public class UserQuizResponse
{
public List<UserQuestionDto> Questions { get; set; } = new();
public int ParticipationPoints { get; set; }
public int CorrectCount { get; set; }
public int TotalQuizPoints { get; set; }
}
public class UserQuestionDto
{
public int Id { get; set; }
public string QuestionText { get; set; } = string.Empty;
public List<string> Options { get; set; } = new();
public int SelectedOption { get; set; }
public bool IsCorrect { get; set; }
}

32
backend/DTOs/UserDtos.cs Normal file
View File

@@ -0,0 +1,32 @@
namespace backend.DTOs;
public class RegisterRequest
{
public string FullName { get; set; } = string.Empty;
public string Contact { get; set; } = string.Empty;
}
public class RegisterResponse
{
public int UserId { get; set; }
public string TrackingCode { get; set; } = string.Empty;
}
public class LoginRequest
{
public string TrackingCode { get; set; } = string.Empty;
}
public class LoginResponse
{
public int UserId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Contact { get; set; } = string.Empty;
public bool IsQuizCompleted { get; set; }
public List<int> KhatmSelections { get; set; } = new();
}
public class UserScoreResponse
{
public int TotalScore { get; set; }
}

View File

@@ -0,0 +1,192 @@
using Microsoft.EntityFrameworkCore;
using backend.Models;
namespace backend.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users => Set<User>();
public DbSet<QuizQuestion> QuizQuestions => Set<QuizQuestion>();
public DbSet<UserQuizAttempt> UserQuizAttempts => Set<UserQuizAttempt>();
public DbSet<UserQuizAnswer> UserQuizAnswers => Set<UserQuizAnswer>();
public DbSet<KhatmItem> KhatmItems => Set<KhatmItem>();
public DbSet<UserKhatmSelection> UserKhatmSelections => Set<UserKhatmSelection>();
public DbSet<UserTotalScore> UserTotalScores => Set<UserTotalScore>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// User
modelBuilder.Entity<User>(entity =>
{
entity.HasIndex(u => u.Contact).IsUnique();
entity.HasIndex(u => u.TrackingCode).IsUnique();
entity.HasOne(u => u.QuizAttempt)
.WithOne(qa => qa.User)
.HasForeignKey<UserQuizAttempt>(qa => qa.UserId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(u => u.TotalScore)
.WithOne(ts => ts.User)
.HasForeignKey<UserTotalScore>(ts => ts.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
// UserQuizAttempt
modelBuilder.Entity<UserQuizAttempt>(entity =>
{
entity.HasIndex(qa => qa.UserId).IsUnique();
entity.HasMany(qa => qa.Answers)
.WithOne(a => a.QuizAttempt)
.HasForeignKey(a => a.UserQuizAttemptId)
.OnDelete(DeleteBehavior.Cascade);
});
// UserQuizAnswer
modelBuilder.Entity<UserQuizAnswer>(entity =>
{
entity.HasOne(a => a.Question)
.WithMany(q => q.Answers)
.HasForeignKey(a => a.QuestionId)
.OnDelete(DeleteBehavior.Cascade);
});
// UserKhatmSelection
modelBuilder.Entity<UserKhatmSelection>(entity =>
{
entity.HasIndex(uk => new { uk.UserId, uk.KhatmItemId }).IsUnique();
entity.HasOne(uk => uk.User)
.WithMany(u => u.KhatmSelections)
.HasForeignKey(uk => uk.UserId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(uk => uk.KhatmItem)
.WithMany(ki => ki.UserSelections)
.HasForeignKey(uk => uk.KhatmItemId)
.OnDelete(DeleteBehavior.Cascade);
});
// UserTotalScore
modelBuilder.Entity<UserTotalScore>(entity =>
{
entity.HasIndex(ts => ts.UserId).IsUnique();
});
// Seed data: KhatmItems
modelBuilder.Entity<KhatmItem>().HasData(
new KhatmItem { Id = 1, Title = "ختم نادعلی", Points = 5, Url = "https://example.com/khatm/nadali" },
new KhatmItem { Id = 2, Title = "خطبه غدیر", Points = 10, Url = "https://example.com/khatm/ghadir" },
new KhatmItem { Id = 3, Title = "دعای توسل", Points = 7, Url = "https://example.com/khatm/tavassol" }
);
// Seed data: QuizQuestions
modelBuilder.Entity<QuizQuestion>().HasData(
new QuizQuestion
{
Id = 1,
QuestionText = "كدام يك از آيات زير مربوط به حجاب نمي باشد؟",
Option1 = "آيه 31 سوره نور",
Option2 = "آيه 58 – 60 سوره نور",
Option3 = "آيه 59 – 60 سوره احزاب",
Option4 = "آيه 59 – 60 سوره نساء",
CorrectOption = 4
},
new QuizQuestion
{
Id = 2,
QuestionText = "اين قسمت از آيه 31 سوره نور « قل للمومنات يغضضن من ابصار هن و يحفظن فروجهن» بيانگر كدام دو تكليف است؟",
Option1 = "پوشش – عفاف",
Option2 = "ترك نظر – پوشش",
Option3 = "ترك نظر – پاكدامني",
Option4 = "پوشش – پاكدامني",
CorrectOption = 3
},
new QuizQuestion
{
Id = 3,
QuestionText = "منظور از (( قواعد )) در سوره نور آيات 58-60 چه كساني هستند؟",
Option1 = "زنان سالخورده اي كه اميدي به ازدواج ايشان نمي باشد",
Option2 = "بردگان و مملوكان",
Option3 = "طفيلياني كه نيازي به زن ندارند",
Option4 = "اطفال نابالغ ولو مميز نزديك به بلوغ",
CorrectOption = 1
},
new QuizQuestion
{
Id = 4,
QuestionText = "هشتمین بخش خطبه غدیر در چه موضوعی است؟",
Option1 = "در مورد ثمرات ولایت و محبت اهل‌بیت",
Option2 = "در مورد جنبه غضب الهی",
Option3 = "در مورد امام مهدی علیه‌السلام",
Option4 = "اقرار لسانی",
CorrectOption = 3
},
new QuizQuestion
{
Id = 5,
QuestionText = "لقب «امیرالمؤمنین» توسط چه کسی به مولا علی علیه‌السلام اطلاق شد؟",
Option1 = "این لقب به دستور مستقیم از جانب خداوند به ایشان داده شد",
Option2 = "پیامبر اکرم، خودشان این لقب را انتخاب کردند",
Option3 = "مردم به علت فضائل بسیار ایشان، این لقب را انتخاب کردند",
Option4 = "به درخواست خود ایشان بود",
CorrectOption = 1
},
new QuizQuestion
{
Id = 6,
QuestionText = "طبق فرمایش پیامبر اکرم در غدیر، عبارت «إِنَّ الإِنسَانَ لَفِي خُسْرٍ»، «انسان در زیان است» درباره چه کسانی است؟",
Option1 = "کسانی که خبر اسلام به آنان نرسیده است",
Option2 = "کسانی که در مدیریت زمان ضعیف هستند",
Option3 = "دشمنان آل محمد (صلی‌الله‌علیه‌وآله)",
Option4 = "اهل کتاب",
CorrectOption = 3
},
new QuizQuestion
{
Id = 7,
QuestionText = "در کدام آیه از قرآن مجید خداوند می فرماید که درباره علی تفریط و کوتاهی نکنید؟",
Option1 = "سوره کهف آیه ۲۸",
Option2 = "سوره انعام آیه ۶۱",
Option3 = "سوره زمر ۵۶",
Option4 = "سوره طه ۴۵",
CorrectOption = 3
},
new QuizQuestion
{
Id = 8,
QuestionText = "طبق آیه 67 سوره مائده وظيفه رسول خدا در برابر امامت چيست و آيه در شأن چه كسی نازل شده است؟\n«يَا أَيُّهَا الرَّسُولُ بَلِّغْ مَا أُنْزِلَ إِلَيْكَ مِنْ رَبِّكَ وَإِنْ لَمْ تَفْعَلْ فَمَا بَلَّغْتَ رِسَالَتَهُ ...»",
Option1 = "تعيين امامت ،علی عليه السلام",
Option2 = "تبليغ امامت، علی عليه السلام",
Option3 = "تعيين و تبليغ امامت، علی عليه السلام",
Option4 = "سپردن تعیین امامت به مردم",
CorrectOption = 2
},
new QuizQuestion
{
Id = 9,
QuestionText = "طبق روایت امام رضا علیه السلام برترین عید امت اسلامی کدام است؟",
Option1 = "عید غدیر، روز تبلیغ و معرفی امیرالمومنین به عنوان جانشین پیامبر",
Option2 = "روز جمعه",
Option3 = "عید قربان، حج تمتع",
Option4 = "عید فطر، پایان یک ماه بندگی",
CorrectOption = 1
},
new QuizQuestion
{
Id = 10,
QuestionText = "با توجه به مطلبی که در آیه شریفه «الْيَوْمَ أَكْمَلْتُ لَكُمْ دِينَكُمْ وَ أَتْمَمْتُ عَلَيْكُمْ نِعْمَتِي وَ رَضِيتُ لَكُمُ الْإِسْلامَ دِينا» در روز غدیر نازل گشته است. کدام گزینه صحیح نمی‌باشد؟",
Option1 = "با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) توسط پیامبر اکرم(صلی الله علیه و آله) دین کامل گشت",
Option2 = "خداوند با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) و یازده فرزند پاکشان از اسلام اعلام رضایت فرمودند",
Option3 = "شرط اینکه دین شخص مسلمان مورد رضایت خداوند متعال قرار گیرد پذیرش ولایت و امامت ائمه (علیهم السلام) است",
Option4 = "امامت از فروع دین بوده و قبول یا عدم پذیرش آن لطمه‌ای به آن شخص وارد نمی‌کند",
CorrectOption = 4
}
);
}
}

25
backend/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# Stage 1: Build the frontend
FROM node:20-alpine AS frontend-build
WORKDIR /app
COPY ClientApp/package*.json ./
RUN npm ci
COPY ClientApp/ .
RUN npm run build
# Stage 2: Build the backend
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS backend-build
WORKDIR /src
COPY ["backend.csproj", "."]
RUN dotnet restore
COPY . .
# Copy the frontend build output from the previous stage
COPY --from=frontend-build /app/dist ./ClientApp/dist
RUN dotnet publish -c Release -o /app/publish
# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
COPY --from=backend-build /app/publish .
ENTRYPOINT ["dotnet", "backend.dll"]

View File

@@ -0,0 +1,374 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using backend.Data;
#nullable disable
namespace backend.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260603125735_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Points")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.ToTable("KhatmItems");
b.HasData(
new
{
Id = 1,
Points = 5,
Title = "ختم نادعلی"
},
new
{
Id = 2,
Points = 10,
Title = "خطبه غدیر"
},
new
{
Id = 3,
Points = 7,
Title = "دعای توسل"
});
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CorrectOption")
.HasColumnType("int");
b.Property<string>("Option1")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option2")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option3")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option4")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("QuestionText")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("QuizQuestions");
b.HasData(
new
{
Id = 1,
CorrectOption = 1,
Option1 = "سال ۱۰ هجری",
Option2 = "سال ۱۱ هجری",
Option3 = "سال ۹ هجری",
Option4 = "سال ۱۲ هجری",
QuestionText = "غدیر در چه سالی واقع شد؟"
},
new
{
Id = 2,
CorrectOption = 3,
Option1 = "ابوبکر",
Option2 = "عمر",
Option3 = "علی (ع)",
Option4 = "عثمان",
QuestionText = "پیامبر اکرم (ص) در غدیر چه کسی را به عنوان جانشین معرفی کردند؟"
},
new
{
Id = 3,
CorrectOption = 2,
Option1 = "مسجد النبی",
Option2 = "غدیر خم",
Option3 = "مکه",
Option4 = "مدینه",
QuestionText = "خطبه غدیر در کجا ایراد شد؟"
});
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Contact")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TrackingCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.HasKey("Id");
b.HasIndex("Contact")
.IsUnique();
b.HasIndex("TrackingCode")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<int>("KhatmItemId")
.HasColumnType("int");
b.Property<DateTime>("LastModified")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("KhatmItemId");
b.HasIndex("UserId", "KhatmItemId")
.IsUnique();
b.ToTable("UserKhatmSelections");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCorrect")
.HasColumnType("bit");
b.Property<int>("QuestionId")
.HasColumnType("int");
b.Property<int>("SelectedOption")
.HasColumnType("int");
b.Property<int>("UserQuizAttemptId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("UserQuizAttemptId");
b.ToTable("UserQuizAnswers");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CorrectAnswersCount")
.HasColumnType("int");
b.Property<int>("ParticipationPoints")
.HasColumnType("int");
b.Property<int>("TotalQuizPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserQuizAttempts");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("TotalPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserTotalScores");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.HasOne("backend.Models.KhatmItem", "KhatmItem")
.WithMany("UserSelections")
.HasForeignKey("KhatmItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.User", "User")
.WithMany("KhatmSelections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("KhatmItem");
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.HasOne("backend.Models.QuizQuestion", "Question")
.WithMany("Answers")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.UserQuizAttempt", "QuizAttempt")
.WithMany("Answers")
.HasForeignKey("UserQuizAttemptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Question");
b.Navigation("QuizAttempt");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("QuizAttempt")
.HasForeignKey("backend.Models.UserQuizAttempt", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("TotalScore")
.HasForeignKey("backend.Models.UserTotalScore", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Navigation("UserSelections");
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Navigation("KhatmSelections");
b.Navigation("QuizAttempt");
b.Navigation("TotalScore");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Navigation("Answers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,253 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace backend.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "KhatmItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Points = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_KhatmItems", x => x.Id);
});
migrationBuilder.CreateTable(
name: "QuizQuestions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
QuestionText = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
Option1 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Option2 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Option3 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Option4 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
CorrectOption = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QuizQuestions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
FullName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Contact = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
TrackingCode = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserKhatmSelections",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<int>(type: "int", nullable: false),
KhatmItemId = table.Column<int>(type: "int", nullable: false),
IsSelected = table.Column<bool>(type: "bit", nullable: false),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserKhatmSelections", x => x.Id);
table.ForeignKey(
name: "FK_UserKhatmSelections_KhatmItems_KhatmItemId",
column: x => x.KhatmItemId,
principalTable: "KhatmItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserKhatmSelections_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserQuizAttempts",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<int>(type: "int", nullable: false),
ParticipationPoints = table.Column<int>(type: "int", nullable: false),
CorrectAnswersCount = table.Column<int>(type: "int", nullable: false),
TotalQuizPoints = table.Column<int>(type: "int", nullable: false),
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserQuizAttempts", x => x.Id);
table.ForeignKey(
name: "FK_UserQuizAttempts_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserTotalScores",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<int>(type: "int", nullable: false),
TotalPoints = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserTotalScores", x => x.Id);
table.ForeignKey(
name: "FK_UserTotalScores_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserQuizAnswers",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserQuizAttemptId = table.Column<int>(type: "int", nullable: false),
QuestionId = table.Column<int>(type: "int", nullable: false),
SelectedOption = table.Column<int>(type: "int", nullable: false),
IsCorrect = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserQuizAnswers", x => x.Id);
table.ForeignKey(
name: "FK_UserQuizAnswers_QuizQuestions_QuestionId",
column: x => x.QuestionId,
principalTable: "QuizQuestions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserQuizAnswers_UserQuizAttempts_UserQuizAttemptId",
column: x => x.UserQuizAttemptId,
principalTable: "UserQuizAttempts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "KhatmItems",
columns: new[] { "Id", "Points", "Title" },
values: new object[,]
{
{ 1, 5, "ختم نادعلی" },
{ 2, 10, "خطبه غدیر" },
{ 3, 7, "دعای توسل" }
});
migrationBuilder.InsertData(
table: "QuizQuestions",
columns: new[] { "Id", "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[,]
{
{ 1, 1, "سال ۱۰ هجری", "سال ۱۱ هجری", "سال ۹ هجری", "سال ۱۲ هجری", "غدیر در چه سالی واقع شد؟" },
{ 2, 3, "ابوبکر", "عمر", "علی (ع)", "عثمان", "پیامبر اکرم (ص) در غدیر چه کسی را به عنوان جانشین معرفی کردند؟" },
{ 3, 2, "مسجد النبی", "غدیر خم", "مکه", "مدینه", "خطبه غدیر در کجا ایراد شد؟" }
});
migrationBuilder.CreateIndex(
name: "IX_UserKhatmSelections_KhatmItemId",
table: "UserKhatmSelections",
column: "KhatmItemId");
migrationBuilder.CreateIndex(
name: "IX_UserKhatmSelections_UserId_KhatmItemId",
table: "UserKhatmSelections",
columns: new[] { "UserId", "KhatmItemId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserQuizAnswers_QuestionId",
table: "UserQuizAnswers",
column: "QuestionId");
migrationBuilder.CreateIndex(
name: "IX_UserQuizAnswers_UserQuizAttemptId",
table: "UserQuizAnswers",
column: "UserQuizAttemptId");
migrationBuilder.CreateIndex(
name: "IX_UserQuizAttempts_UserId",
table: "UserQuizAttempts",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Contact",
table: "Users",
column: "Contact",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_TrackingCode",
table: "Users",
column: "TrackingCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserTotalScores_UserId",
table: "UserTotalScores",
column: "UserId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserKhatmSelections");
migrationBuilder.DropTable(
name: "UserQuizAnswers");
migrationBuilder.DropTable(
name: "UserTotalScores");
migrationBuilder.DropTable(
name: "KhatmItems");
migrationBuilder.DropTable(
name: "QuizQuestions");
migrationBuilder.DropTable(
name: "UserQuizAttempts");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -0,0 +1,381 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using backend.Data;
#nullable disable
namespace backend.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260603142557_AddKhatmItemUrl")]
partial class AddKhatmItemUrl
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Points")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Url")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("KhatmItems");
b.HasData(
new
{
Id = 1,
Points = 5,
Title = "ختم نادعلی",
Url = "https://example.com/khatm/nadali"
},
new
{
Id = 2,
Points = 10,
Title = "خطبه غدیر",
Url = "https://example.com/khatm/ghadir"
},
new
{
Id = 3,
Points = 7,
Title = "دعای توسل",
Url = "https://example.com/khatm/tavassol"
});
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CorrectOption")
.HasColumnType("int");
b.Property<string>("Option1")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option2")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option3")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option4")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("QuestionText")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("QuizQuestions");
b.HasData(
new
{
Id = 1,
CorrectOption = 1,
Option1 = "سال ۱۰ هجری",
Option2 = "سال ۱۱ هجری",
Option3 = "سال ۹ هجری",
Option4 = "سال ۱۲ هجری",
QuestionText = "غدیر در چه سالی واقع شد؟"
},
new
{
Id = 2,
CorrectOption = 3,
Option1 = "ابوبکر",
Option2 = "عمر",
Option3 = "علی (ع)",
Option4 = "عثمان",
QuestionText = "پیامبر اکرم (ص) در غدیر چه کسی را به عنوان جانشین معرفی کردند؟"
},
new
{
Id = 3,
CorrectOption = 2,
Option1 = "مسجد النبی",
Option2 = "غدیر خم",
Option3 = "مکه",
Option4 = "مدینه",
QuestionText = "خطبه غدیر در کجا ایراد شد؟"
});
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Contact")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TrackingCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.HasKey("Id");
b.HasIndex("Contact")
.IsUnique();
b.HasIndex("TrackingCode")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<int>("KhatmItemId")
.HasColumnType("int");
b.Property<DateTime>("LastModified")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("KhatmItemId");
b.HasIndex("UserId", "KhatmItemId")
.IsUnique();
b.ToTable("UserKhatmSelections");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCorrect")
.HasColumnType("bit");
b.Property<int>("QuestionId")
.HasColumnType("int");
b.Property<int>("SelectedOption")
.HasColumnType("int");
b.Property<int>("UserQuizAttemptId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("UserQuizAttemptId");
b.ToTable("UserQuizAnswers");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CorrectAnswersCount")
.HasColumnType("int");
b.Property<int>("ParticipationPoints")
.HasColumnType("int");
b.Property<int>("TotalQuizPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserQuizAttempts");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("TotalPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserTotalScores");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.HasOne("backend.Models.KhatmItem", "KhatmItem")
.WithMany("UserSelections")
.HasForeignKey("KhatmItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.User", "User")
.WithMany("KhatmSelections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("KhatmItem");
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.HasOne("backend.Models.QuizQuestion", "Question")
.WithMany("Answers")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.UserQuizAttempt", "QuizAttempt")
.WithMany("Answers")
.HasForeignKey("UserQuizAttemptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Question");
b.Navigation("QuizAttempt");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("QuizAttempt")
.HasForeignKey("backend.Models.UserQuizAttempt", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("TotalScore")
.HasForeignKey("backend.Models.UserTotalScore", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Navigation("UserSelections");
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Navigation("KhatmSelections");
b.Navigation("QuizAttempt");
b.Navigation("TotalScore");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Navigation("Answers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace backend.Migrations
{
/// <inheritdoc />
public partial class AddKhatmItemUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Url",
table: "KhatmItems",
type: "nvarchar(500)",
maxLength: 500,
nullable: true);
migrationBuilder.UpdateData(
table: "KhatmItems",
keyColumn: "Id",
keyValue: 1,
column: "Url",
value: "https://example.com/khatm/nadali");
migrationBuilder.UpdateData(
table: "KhatmItems",
keyColumn: "Id",
keyValue: 2,
column: "Url",
value: "https://example.com/khatm/ghadir");
migrationBuilder.UpdateData(
table: "KhatmItems",
keyColumn: "Id",
keyValue: 3,
column: "Url",
value: "https://example.com/khatm/tavassol");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Url",
table: "KhatmItems");
}
}
}

View File

@@ -0,0 +1,381 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using backend.Data;
#nullable disable
namespace backend.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260603144334_UpdateTrackingCodeLength")]
partial class UpdateTrackingCodeLength
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Points")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Url")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("KhatmItems");
b.HasData(
new
{
Id = 1,
Points = 5,
Title = "ختم نادعلی",
Url = "https://example.com/khatm/nadali"
},
new
{
Id = 2,
Points = 10,
Title = "خطبه غدیر",
Url = "https://example.com/khatm/ghadir"
},
new
{
Id = 3,
Points = 7,
Title = "دعای توسل",
Url = "https://example.com/khatm/tavassol"
});
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CorrectOption")
.HasColumnType("int");
b.Property<string>("Option1")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option2")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option3")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option4")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("QuestionText")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("QuizQuestions");
b.HasData(
new
{
Id = 1,
CorrectOption = 1,
Option1 = "سال ۱۰ هجری",
Option2 = "سال ۱۱ هجری",
Option3 = "سال ۹ هجری",
Option4 = "سال ۱۲ هجری",
QuestionText = "غدیر در چه سالی واقع شد؟"
},
new
{
Id = 2,
CorrectOption = 3,
Option1 = "ابوبکر",
Option2 = "عمر",
Option3 = "علی (ع)",
Option4 = "عثمان",
QuestionText = "پیامبر اکرم (ص) در غدیر چه کسی را به عنوان جانشین معرفی کردند؟"
},
new
{
Id = 3,
CorrectOption = 2,
Option1 = "مسجد النبی",
Option2 = "غدیر خم",
Option3 = "مکه",
Option4 = "مدینه",
QuestionText = "خطبه غدیر در کجا ایراد شد؟"
});
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Contact")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TrackingCode")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.HasKey("Id");
b.HasIndex("Contact")
.IsUnique();
b.HasIndex("TrackingCode")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<int>("KhatmItemId")
.HasColumnType("int");
b.Property<DateTime>("LastModified")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("KhatmItemId");
b.HasIndex("UserId", "KhatmItemId")
.IsUnique();
b.ToTable("UserKhatmSelections");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCorrect")
.HasColumnType("bit");
b.Property<int>("QuestionId")
.HasColumnType("int");
b.Property<int>("SelectedOption")
.HasColumnType("int");
b.Property<int>("UserQuizAttemptId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("UserQuizAttemptId");
b.ToTable("UserQuizAnswers");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CorrectAnswersCount")
.HasColumnType("int");
b.Property<int>("ParticipationPoints")
.HasColumnType("int");
b.Property<int>("TotalQuizPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserQuizAttempts");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("TotalPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserTotalScores");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.HasOne("backend.Models.KhatmItem", "KhatmItem")
.WithMany("UserSelections")
.HasForeignKey("KhatmItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.User", "User")
.WithMany("KhatmSelections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("KhatmItem");
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.HasOne("backend.Models.QuizQuestion", "Question")
.WithMany("Answers")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.UserQuizAttempt", "QuizAttempt")
.WithMany("Answers")
.HasForeignKey("UserQuizAttemptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Question");
b.Navigation("QuizAttempt");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("QuizAttempt")
.HasForeignKey("backend.Models.UserQuizAttempt", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("TotalScore")
.HasForeignKey("backend.Models.UserTotalScore", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Navigation("UserSelections");
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Navigation("KhatmSelections");
b.Navigation("QuizAttempt");
b.Navigation("TotalScore");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Navigation("Answers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace backend.Migrations
{
/// <inheritdoc />
public partial class UpdateTrackingCodeLength : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// First, update existing tracking codes to 4-digit numeric values
// Using a hash-based approach to generate unique 4-digit codes from existing ones
migrationBuilder.Sql(@"
DECLARE @Id int, @RowNum int, @NewCode nvarchar(4)
DECLARE code_cursor CURSOR FOR
SELECT Id, ROW_NUMBER() OVER (ORDER BY Id) AS RowNum
FROM Users
OPEN code_cursor
FETCH NEXT FROM code_cursor INTO @Id, @RowNum
WHILE @@FETCH_STATUS = 0
BEGIN
SET @NewCode = RIGHT('0000' + CAST((@RowNum + 1000) AS nvarchar(4)), 4)
UPDATE Users SET TrackingCode = @NewCode WHERE Id = @Id
FETCH NEXT FROM code_cursor INTO @Id, @RowNum
END
CLOSE code_cursor
DEALLOCATE code_cursor
");
migrationBuilder.AlterColumn<string>(
name: "TrackingCode",
table: "Users",
type: "nvarchar(4)",
maxLength: 4,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(8)",
oldMaxLength: 8);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "TrackingCode",
table: "Users",
type: "nvarchar(8)",
maxLength: 8,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(4)",
oldMaxLength: 4);
}
}
}

View File

@@ -0,0 +1,451 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using backend.Data;
#nullable disable
namespace backend.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260603144804_SeedQuizQuestions")]
partial class SeedQuizQuestions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Points")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Url")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("KhatmItems");
b.HasData(
new
{
Id = 1,
Points = 5,
Title = "ختم نادعلی",
Url = "https://example.com/khatm/nadali"
},
new
{
Id = 2,
Points = 10,
Title = "خطبه غدیر",
Url = "https://example.com/khatm/ghadir"
},
new
{
Id = 3,
Points = 7,
Title = "دعای توسل",
Url = "https://example.com/khatm/tavassol"
});
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CorrectOption")
.HasColumnType("int");
b.Property<string>("Option1")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option2")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option3")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option4")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("QuestionText")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("QuizQuestions");
b.HasData(
new
{
Id = 1,
CorrectOption = 4,
Option1 = "آيه 31 سوره نور",
Option2 = "آيه 58 – 60 سوره نور",
Option3 = "آيه 59 – 60 سوره احزاب",
Option4 = "آيه 59 – 60 سوره نساء",
QuestionText = "كدام يك از آيات زير مربوط به حجاب نمي باشد؟"
},
new
{
Id = 2,
CorrectOption = 3,
Option1 = "پوشش – عفاف",
Option2 = "ترك نظر – پوشش",
Option3 = "ترك نظر – پاكدامني",
Option4 = "پوشش – پاكدامني",
QuestionText = "اين قسمت از آيه 31 سوره نور « قل للمومنات يغضضن من ابصار هن و يحفظن فروجهن» بيانگر كدام دو تكليف است؟"
},
new
{
Id = 3,
CorrectOption = 1,
Option1 = "زنان سالخورده اي كه اميدي به ازدواج ايشان نمي باشد",
Option2 = "بردگان و مملوكان",
Option3 = "طفيلياني كه نيازي به زن ندارند",
Option4 = "اطفال نابالغ ولو مميز نزديك به بلوغ",
QuestionText = "منظور از (( قواعد )) در سوره نور آيات 58-60 چه كساني هستند؟"
},
new
{
Id = 4,
CorrectOption = 3,
Option1 = "در مورد ثمرات ولایت و محبت اهل‌بیت",
Option2 = "در مورد جنبه غضب الهی",
Option3 = "در مورد امام مهدی علیه‌السلام",
Option4 = "اقرار لسانی",
QuestionText = "هشتمین بخش خطبه غدیر در چه موضوعی است؟"
},
new
{
Id = 5,
CorrectOption = 1,
Option1 = "این لقب به دستور مستقیم از جانب خداوند به ایشان داده شد",
Option2 = "پیامبر اکرم، خودشان این لقب را انتخاب کردند",
Option3 = "مردم به علت فضائل بسیار ایشان، این لقب را انتخاب کردند",
Option4 = "به درخواست خود ایشان بود",
QuestionText = "لقب «امیرالمؤمنین» توسط چه کسی به مولا علی علیه‌السلام اطلاق شد؟"
},
new
{
Id = 6,
CorrectOption = 3,
Option1 = "کسانی که خبر اسلام به آنان نرسیده است",
Option2 = "کسانی که در مدیریت زمان ضعیف هستند",
Option3 = "دشمنان آل محمد (صلی‌الله‌علیه‌وآله)",
Option4 = "اهل کتاب",
QuestionText = "طبق فرمایش پیامبر اکرم در غدیر، عبارت «إِنَّ الإِنسَانَ لَفِي خُسْرٍ»، «انسان در زیان است» درباره چه کسانی است؟"
},
new
{
Id = 7,
CorrectOption = 3,
Option1 = "سوره کهف آیه ۲۸",
Option2 = "سوره انعام آیه ۶۱",
Option3 = "سوره زمر ۵۶",
Option4 = "سوره طه ۴۵",
QuestionText = "در کدام آیه از قرآن مجید خداوند می فرماید که درباره علی تفریط و کوتاهی نکنید؟"
},
new
{
Id = 8,
CorrectOption = 2,
Option1 = "تعيين امامت ،علی عليه السلام",
Option2 = "تبليغ امامت، علی عليه السلام",
Option3 = "تعيين و تبليغ امامت، علی عليه السلام",
Option4 = "سپردن تعیین امامت به مردم",
QuestionText = "طبق آیه 67 سوره مائده وظيفه رسول خدا در برابر امامت چيست و آيه در شأن چه كسی نازل شده است؟\n«يَا أَيُّهَا الرَّسُولُ بَلِّغْ مَا أُنْزِلَ إِلَيْكَ مِنْ رَبِّكَ وَإِنْ لَمْ تَفْعَلْ فَمَا بَلَّغْتَ رِسَالَتَهُ ...»"
},
new
{
Id = 9,
CorrectOption = 1,
Option1 = "عید غدیر، روز تبلیغ و معرفی امیرالمومنین به عنوان جانشین پیامبر",
Option2 = "روز جمعه",
Option3 = "عید قربان، حج تمتع",
Option4 = "عید فطر، پایان یک ماه بندگی",
QuestionText = "طبق روایت امام رضا علیه السلام برترین عید امت اسلامی کدام است؟"
},
new
{
Id = 10,
CorrectOption = 4,
Option1 = "با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) توسط پیامبر اکرم(صلی الله علیه و آله) دین کامل گشت",
Option2 = "خداوند با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) و یازده فرزند پاکشان از اسلام اعلام رضایت فرمودند",
Option3 = "شرط اینکه دین شخص مسلمان مورد رضایت خداوند متعال قرار گیرد پذیرش ولایت و امامت ائمه (علیهم السلام) است",
Option4 = "امامت از فروع دین بوده و قبول یا عدم پذیرش آن لطمه‌ای به آن شخص وارد نمی‌کند",
QuestionText = "با توجه به مطلبی که در آیه شریفه «الْيَوْمَ أَكْمَلْتُ لَكُمْ دِينَكُمْ وَ أَتْمَمْتُ عَلَيْكُمْ نِعْمَتِي وَ رَضِيتُ لَكُمُ الْإِسْلامَ دِينا» در روز غدیر نازل گشته است. کدام گزینه صحیح نمی‌باشد؟"
});
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Contact")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TrackingCode")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.HasKey("Id");
b.HasIndex("Contact")
.IsUnique();
b.HasIndex("TrackingCode")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<int>("KhatmItemId")
.HasColumnType("int");
b.Property<DateTime>("LastModified")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("KhatmItemId");
b.HasIndex("UserId", "KhatmItemId")
.IsUnique();
b.ToTable("UserKhatmSelections");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCorrect")
.HasColumnType("bit");
b.Property<int>("QuestionId")
.HasColumnType("int");
b.Property<int>("SelectedOption")
.HasColumnType("int");
b.Property<int>("UserQuizAttemptId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("UserQuizAttemptId");
b.ToTable("UserQuizAnswers");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CorrectAnswersCount")
.HasColumnType("int");
b.Property<int>("ParticipationPoints")
.HasColumnType("int");
b.Property<int>("TotalQuizPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserQuizAttempts");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("TotalPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserTotalScores");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.HasOne("backend.Models.KhatmItem", "KhatmItem")
.WithMany("UserSelections")
.HasForeignKey("KhatmItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.User", "User")
.WithMany("KhatmSelections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("KhatmItem");
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.HasOne("backend.Models.QuizQuestion", "Question")
.WithMany("Answers")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.UserQuizAttempt", "QuizAttempt")
.WithMany("Answers")
.HasForeignKey("UserQuizAttemptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Question");
b.Navigation("QuizAttempt");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("QuizAttempt")
.HasForeignKey("backend.Models.UserQuizAttempt", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("TotalScore")
.HasForeignKey("backend.Models.UserTotalScore", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Navigation("UserSelections");
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Navigation("KhatmSelections");
b.Navigation("QuizAttempt");
b.Navigation("TotalScore");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Navigation("Answers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,111 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace backend.Migrations
{
/// <inheritdoc />
public partial class SeedQuizQuestions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { 4, "آيه 31 سوره نور", "آيه 58 – 60 سوره نور", "آيه 59 – 60 سوره احزاب", "آيه 59 – 60 سوره نساء", "كدام يك از آيات زير مربوط به حجاب نمي باشد؟" });
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { "پوشش – عفاف", "ترك نظر – پوشش", "ترك نظر – پاكدامني", "پوشش – پاكدامني", "اين قسمت از آيه 31 سوره نور « قل للمومنات يغضضن من ابصار هن و يحفظن فروجهن» بيانگر كدام دو تكليف است؟" });
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 3,
columns: new[] { "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { 1, "زنان سالخورده اي كه اميدي به ازدواج ايشان نمي باشد", "بردگان و مملوكان", "طفيلياني كه نيازي به زن ندارند", "اطفال نابالغ ولو مميز نزديك به بلوغ", "منظور از (( قواعد )) در سوره نور آيات 58-60 چه كساني هستند؟" });
migrationBuilder.InsertData(
table: "QuizQuestions",
columns: new[] { "Id", "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[,]
{
{ 4, 3, "در مورد ثمرات ولایت و محبت اهل‌بیت", "در مورد جنبه غضب الهی", "در مورد امام مهدی علیه‌السلام", "اقرار لسانی", "هشتمین بخش خطبه غدیر در چه موضوعی است؟" },
{ 5, 1, "این لقب به دستور مستقیم از جانب خداوند به ایشان داده شد", "پیامبر اکرم، خودشان این لقب را انتخاب کردند", "مردم به علت فضائل بسیار ایشان، این لقب را انتخاب کردند", "به درخواست خود ایشان بود", "لقب «امیرالمؤمنین» توسط چه کسی به مولا علی علیه‌السلام اطلاق شد؟" },
{ 6, 3, "کسانی که خبر اسلام به آنان نرسیده است", "کسانی که در مدیریت زمان ضعیف هستند", "دشمنان آل محمد (صلی‌الله‌علیه‌وآله)", "اهل کتاب", "طبق فرمایش پیامبر اکرم در غدیر، عبارت «إِنَّ الإِنسَانَ لَفِي خُسْرٍ»، «انسان در زیان است» درباره چه کسانی است؟" },
{ 7, 3, "سوره کهف آیه ۲۸", "سوره انعام آیه ۶۱", "سوره زمر ۵۶", "سوره طه ۴۵", "در کدام آیه از قرآن مجید خداوند می فرماید که درباره علی تفریط و کوتاهی نکنید؟" },
{ 8, 2, "تعيين امامت ،علی عليه السلام", "تبليغ امامت، علی عليه السلام", "تعيين و تبليغ امامت، علی عليه السلام", "سپردن تعیین امامت به مردم", "طبق آیه 67 سوره مائده وظيفه رسول خدا در برابر امامت چيست و آيه در شأن چه كسی نازل شده است؟\n«يَا أَيُّهَا الرَّسُولُ بَلِّغْ مَا أُنْزِلَ إِلَيْكَ مِنْ رَبِّكَ وَإِنْ لَمْ تَفْعَلْ فَمَا بَلَّغْتَ رِسَالَتَهُ ...»" },
{ 9, 1, "عید غدیر، روز تبلیغ و معرفی امیرالمومنین به عنوان جانشین پیامبر", "روز جمعه", "عید قربان، حج تمتع", "عید فطر، پایان یک ماه بندگی", "طبق روایت امام رضا علیه السلام برترین عید امت اسلامی کدام است؟" },
{ 10, 4, "با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) توسط پیامبر اکرم(صلی الله علیه و آله) دین کامل گشت", "خداوند با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) و یازده فرزند پاکشان از اسلام اعلام رضایت فرمودند", "شرط اینکه دین شخص مسلمان مورد رضایت خداوند متعال قرار گیرد پذیرش ولایت و امامت ائمه (علیهم السلام) است", "امامت از فروع دین بوده و قبول یا عدم پذیرش آن لطمه‌ای به آن شخص وارد نمی‌کند", "با توجه به مطلبی که در آیه شریفه «الْيَوْمَ أَكْمَلْتُ لَكُمْ دِينَكُمْ وَ أَتْمَمْتُ عَلَيْكُمْ نِعْمَتِي وَ رَضِيتُ لَكُمُ الْإِسْلامَ دِينا» در روز غدیر نازل گشته است. کدام گزینه صحیح نمی‌باشد؟" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 4);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 5);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 6);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 7);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 8);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 9);
migrationBuilder.DeleteData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 10);
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { 1, "سال ۱۰ هجری", "سال ۱۱ هجری", "سال ۹ هجری", "سال ۱۲ هجری", "غدیر در چه سالی واقع شد؟" });
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { "ابوبکر", "عمر", "علی (ع)", "عثمان", "پیامبر اکرم (ص) در غدیر چه کسی را به عنوان جانشین معرفی کردند؟" });
migrationBuilder.UpdateData(
table: "QuizQuestions",
keyColumn: "Id",
keyValue: 3,
columns: new[] { "CorrectOption", "Option1", "Option2", "Option3", "Option4", "QuestionText" },
values: new object[] { 2, "مسجد النبی", "غدیر خم", "مکه", "مدینه", "خطبه غدیر در کجا ایراد شد؟" });
}
}
}

View File

@@ -0,0 +1,448 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using backend.Data;
#nullable disable
namespace backend.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Points")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Url")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("KhatmItems");
b.HasData(
new
{
Id = 1,
Points = 5,
Title = "ختم نادعلی",
Url = "https://example.com/khatm/nadali"
},
new
{
Id = 2,
Points = 10,
Title = "خطبه غدیر",
Url = "https://example.com/khatm/ghadir"
},
new
{
Id = 3,
Points = 7,
Title = "دعای توسل",
Url = "https://example.com/khatm/tavassol"
});
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CorrectOption")
.HasColumnType("int");
b.Property<string>("Option1")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option2")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option3")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Option4")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("QuestionText")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("QuizQuestions");
b.HasData(
new
{
Id = 1,
CorrectOption = 3,
Option1 = "در مورد ثمرات ولایت و محبت اهل‌بیت",
Option2 = "در مورد جنبه غضب الهی",
Option3 = "در مورد امام مهدی علیه‌السلام",
Option4 = "اقرار لسانی",
QuestionText = "هشتمین بخش خطبه غدیر در چه موضوعی است؟"
},
new
{
Id = 2,
CorrectOption = 1,
Option1 = "این لقب به دستور مستقیم از جانب خداوند به ایشان داده شد",
Option2 = "پیامبر اکرم، خودشان این لقب را انتخاب کردند",
Option3 = "مردم به علت فضائل بسیار ایشان، این لقب را انتخاب کردند",
Option4 = "به درخواست خود ایشان بود",
QuestionText = "لقب «امیرالمؤمنین» توسط چه کسی به مولا علی علیه‌السلام اطلاق شد؟"
},
new
{
Id = 3,
CorrectOption = 3,
Option1 = "کسانی که خبر اسلام به آنان نرسیده است",
Option2 = "کسانی که در مدیریت زمان ضعیف هستند",
Option3 = "دشمنان آل محمد (صلی‌الله‌علیه‌وآله)",
Option4 = "اهل کتاب",
QuestionText = "طبق فرمایش پیامبر اکرم در غدیر، عبارت «إِنَّ الإِنسَانَ لَفِي خُسْرٍ»، «انسان در زیان است» درباره چه کسانی است؟"
},
new
{
Id = 4,
CorrectOption = 4,
Option1 = "آيه 31 سوره نور",
Option2 = "آيه 58 – 60 سوره نور",
Option3 = "آيه 59 – 60 سوره احزاب",
Option4 = "آيه 59 – 60 سوره نساء",
QuestionText = "كدام يك از آيات زير مربوط به حجاب نمي باشد؟"
},
new
{
Id = 5,
CorrectOption = 3,
Option1 = "پوشش – عفاف",
Option2 = "ترك نظر – پوشش",
Option3 = "ترك نظر – پاكدامني",
Option4 = "پوشش – پاكدامني",
QuestionText = "اين قسمت از آيه 31 سوره نور « قل للمومنات يغضضن من ابصار هن و يحفظن فروجهن» بيانگر كدام دو تكليف است؟"
},
new
{
Id = 6,
CorrectOption = 1,
Option1 = "زنان سالخورده اي كه اميدي به ازدواج ايشان نمي باشد",
Option2 = "بردگان و مملوكان",
Option3 = "طفيلياني كه نيازي به زن ندارند",
Option4 = "اطفال نابالغ ولو مميز نزديك به بلوغ",
QuestionText = "منظور از (( قواعد )) در سوره نور آيات 58-60 چه كساني هستند؟"
},
new
{
Id = 7,
CorrectOption = 3,
Option1 = "سوره کهف آیه ۲۸",
Option2 = "سوره انعام آیه ۶۱",
Option3 = "سوره زمر ۵۶",
Option4 = "سوره طه ۴۵",
QuestionText = "در کدام آیه از قرآن مجید خداوند می فرماید که درباره علی تفریط و کوتاهی نکنید؟"
},
new
{
Id = 8,
CorrectOption = 2,
Option1 = "تعيين امامت ،علی عليه السلام",
Option2 = "تبليغ امامت، علی عليه السلام",
Option3 = "تعيين و تبليغ امامت، علی عليه السلام",
Option4 = "سپردن تعیین امامت به مردم",
QuestionText = "طبق آیه 67 سوره مائده وظيفه رسول خدا در برابر امامت چيست و آيه در شأن چه كسی نازل شده است؟\n«يَا أَيُّهَا الرَّسُولُ بَلِّغْ مَا أُنْزِلَ إِلَيْكَ مِنْ رَبِّكَ وَإِنْ لَمْ تَفْعَلْ فَمَا بَلَّغْتَ رِسَالَتَهُ ...»"
},
new
{
Id = 9,
CorrectOption = 1,
Option1 = "عید غدیر، روز تبلیغ و معرفی امیرالمومنین به عنوان جانشین پیامبر",
Option2 = "روز جمعه",
Option3 = "عید قربان، حج تمتع",
Option4 = "عید فطر، پایان یک ماه بندگی",
QuestionText = "طبق روایت امام رضا علیه السلام برترین عید امت اسلامی کدام است؟"
},
new
{
Id = 10,
CorrectOption = 4,
Option1 = "با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) توسط پیامبر اکرم(صلی الله علیه و آله) دین کامل گشت",
Option2 = "خداوند با ابلاغ ولایت و امامت امیرالمومنین(علیه السلام) و یازده فرزند پاکشان از اسلام اعلام رضایت فرمودند",
Option3 = "شرط اینکه دین شخص مسلمان مورد رضایت خداوند متعال قرار گیرد پذیرش ولایت و امامت ائمه (علیهم السلام) است",
Option4 = "امامت از فروع دین بوده و قبول یا عدم پذیرش آن لطمه‌ای به آن شخص وارد نمی‌کند",
QuestionText = "با توجه به مطلبی که در آیه شریفه «الْيَوْمَ أَكْمَلْتُ لَكُمْ دِينَكُمْ وَ أَتْمَمْتُ عَلَيْكُمْ نِعْمَتِي وَ رَضِيتُ لَكُمُ الْإِسْلامَ دِينا» در روز غدیر نازل گشته است. کدام گزینه صحیح نمی‌باشد؟"
});
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Contact")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TrackingCode")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.HasKey("Id");
b.HasIndex("Contact")
.IsUnique();
b.HasIndex("TrackingCode")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<int>("KhatmItemId")
.HasColumnType("int");
b.Property<DateTime>("LastModified")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("KhatmItemId");
b.HasIndex("UserId", "KhatmItemId")
.IsUnique();
b.ToTable("UserKhatmSelections");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCorrect")
.HasColumnType("bit");
b.Property<int>("QuestionId")
.HasColumnType("int");
b.Property<int>("SelectedOption")
.HasColumnType("int");
b.Property<int>("UserQuizAttemptId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("UserQuizAttemptId");
b.ToTable("UserQuizAnswers");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CorrectAnswersCount")
.HasColumnType("int");
b.Property<int>("ParticipationPoints")
.HasColumnType("int");
b.Property<int>("TotalQuizPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserQuizAttempts");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("TotalPoints")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("UserTotalScores");
});
modelBuilder.Entity("backend.Models.UserKhatmSelection", b =>
{
b.HasOne("backend.Models.KhatmItem", "KhatmItem")
.WithMany("UserSelections")
.HasForeignKey("KhatmItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.User", "User")
.WithMany("KhatmSelections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("KhatmItem");
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserQuizAnswer", b =>
{
b.HasOne("backend.Models.QuizQuestion", "Question")
.WithMany("Answers")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("backend.Models.UserQuizAttempt", "QuizAttempt")
.WithMany("Answers")
.HasForeignKey("UserQuizAttemptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Question");
b.Navigation("QuizAttempt");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("QuizAttempt")
.HasForeignKey("backend.Models.UserQuizAttempt", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.UserTotalScore", b =>
{
b.HasOne("backend.Models.User", "User")
.WithOne("TotalScore")
.HasForeignKey("backend.Models.UserTotalScore", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.KhatmItem", b =>
{
b.Navigation("UserSelections");
});
modelBuilder.Entity("backend.Models.QuizQuestion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("backend.Models.User", b =>
{
b.Navigation("KhatmSelections");
b.Navigation("QuizAttempt");
b.Navigation("TotalScore");
});
modelBuilder.Entity("backend.Models.UserQuizAttempt", b =>
{
b.Navigation("Answers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,23 @@
namespace backend.Models;
public class AppConfig
{
public string SiteTitle { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public List<ChannelInfo> Channels { get; set; } = new();
public string ShortText { get; set; } = string.Empty;
public QuizConfig Quiz { get; set; } = new();
public string AdminPassword { get; set; } = string.Empty;
}
public class ChannelInfo
{
public string Name { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
public class QuizConfig
{
public int ParticipationPoints { get; set; }
public int CorrectAnswerPoints { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class KhatmItem
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; } = string.Empty;
[MaxLength(500)]
public string? Url { get; set; }
[Required]
public int Points { get; set; }
// Navigation
public ICollection<UserKhatmSelection> UserSelections { get; set; } = new List<UserKhatmSelection>();
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class QuizQuestion
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(500)]
public string QuestionText { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string Option1 { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string Option2 { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string Option3 { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string Option4 { get; set; } = string.Empty;
[Required]
public int CorrectOption { get; set; } // 1-4
// Navigation
public ICollection<UserQuizAnswer> Answers { get; set; } = new List<UserQuizAnswer>();
}

28
backend/Models/User.cs Normal file
View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string FullName { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Contact { get; set; } = string.Empty;
[Required]
[MaxLength(4)]
public string TrackingCode { get; set; } = string.Empty;
// Navigation properties
public UserQuizAttempt? QuizAttempt { get; set; }
public ICollection<UserKhatmSelection> KhatmSelections { get; set; } = new List<UserKhatmSelection>();
public UserTotalScore? TotalScore { get; set; }
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class UserKhatmSelection
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public int KhatmItemId { get; set; }
public bool IsSelected { get; set; }
public DateTime LastModified { get; set; } = DateTime.UtcNow;
// Navigation
[ForeignKey(nameof(UserId))]
public User User { get; set; } = null!;
[ForeignKey(nameof(KhatmItemId))]
public KhatmItem KhatmItem { get; set; } = null!;
}

View File

@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class UserQuizAnswer
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public int UserQuizAttemptId { get; set; }
[Required]
public int QuestionId { get; set; }
[Required]
public int SelectedOption { get; set; }
public bool IsCorrect { get; set; }
// Navigation
[ForeignKey(nameof(UserQuizAttemptId))]
public UserQuizAttempt QuizAttempt { get; set; } = null!;
[ForeignKey(nameof(QuestionId))]
public QuizQuestion Question { get; set; } = null!;
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class UserQuizAttempt
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public int UserId { get; set; }
public int ParticipationPoints { get; set; }
public int CorrectAnswersCount { get; set; }
public int TotalQuizPoints { get; set; }
public DateTime CompletedAt { get; set; } = DateTime.UtcNow;
// Navigation
[ForeignKey(nameof(UserId))]
public User User { get; set; } = null!;
public ICollection<UserQuizAnswer> Answers { get; set; } = new List<UserQuizAnswer>();
}

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace backend.Models;
public class UserTotalScore
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public int UserId { get; set; }
public int TotalPoints { get; set; }
// Navigation
[ForeignKey(nameof(UserId))]
public User User { get; set; } = null!;
}

64
backend/Program.cs Normal file
View File

@@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore;
using backend.Data;
using backend.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
// Configure Entity Framework with SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Bind AppConfig from appsettings.json
builder.Services.Configure<AppConfig>(
builder.Configuration.GetSection("AppConfig"));
// Configure CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:5173", "http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Configure serving the built SPA static files (used in Production)
builder.Services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
var app = builder.Build();
app.UseCors("AllowFrontend");
app.MapControllers();
// Auto-migrate database on startup
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
// Configure SPA serving
if (!app.Environment.IsDevelopment())
{
// In Production/Staging, serve the pre-built frontend from ClientApp/dist
app.UseSpaStaticFiles();
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
});
}
// In Development:
// - The backend serves only the API at http://localhost:5000
// - The frontend is served by Vite dev server at http://localhost:5173
// - Vite proxies /api requests to the backend
// - Run: cd backend\ClientApp && npm run dev (for hot-reload frontend)
// - Or open http://localhost:5000 after building the frontend
app.Run();

View File

@@ -0,0 +1,28 @@
{
"profiles": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5000"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53255/",
"sslPort": 44327
}
}
}

32
backend/appsettings.json Normal file
View File

@@ -0,0 +1,32 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=GhadirContestDB;Trusted_Connection=True;TrustServerCertificate=True"
},
"AppConfig": {
"SiteTitle": "مسابقه بزرگ غدیر",
"Subtitle": "هیئت ثارالله با همکاری کانال شهر من کهک برگزار می کند.",
"Channels": [
{
"Name": "هیئت ثارالله",
"Url": "https://eitaa.com/saralllah_kahak"
},
{
"Name": "کانال کهک",
"Url": "https://eitaa.com/kahakcity"
}
],
"ShortText": "به مسابقه بزرگ غدیر خوش آمدید. با شرکت در این مسابقه، ضمن آشنایی بیشتر با واقعه غدیر، می‌توانید در قرعه‌کشی جوایز ارزنده شرکت کنید.",
"Quiz": {
"ParticipationPoints": 10,
"CorrectAnswerPoints": 5
},
"AdminPassword": "ghadir1403"
}
}

59
backend/backend.csproj Normal file
View File

@@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SpaRoot>ClientApp\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- Include the SPA project for publishing -->
<ItemGroup>
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build the frontend. See https://nodejs.org" />
<Message Importance="high" Text="Restoring frontend dependencies using npm..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- Build the frontend -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" Condition=" !Exists('$(SpaRoot)node_modules') " />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<Error Condition="'$(ErrorCode)' != '0'" Text="Frontend build failed." />
<!-- Include the built frontend in publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

6
backend/backend.http Normal file
View File

@@ -0,0 +1,6 @@
@backend_HostAddress = http://localhost:5004
GET {{backend_HostAddress}}/weatherforecast/
Accept: application/json
###

47
docker-compose.yml Normal file
View File

@@ -0,0 +1,47 @@
version: "3.8"
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: ghadir-db
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=Ghadir@1403
- MSSQL_PID=Express
ports:
- "1433:1433"
volumes:
- sqlserver-data:/var/opt/mssql
networks:
- ghadir-network
healthcheck:
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Ghadir@1403" -C -Q "SELECT 1" || exit 1
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: ghadir-backend
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=GhadirContestDB;User Id=sa;Password=Ghadir@1403;TrustServerCertificate=True
ports:
- "5000:8080"
depends_on:
sqlserver:
condition: service_healthy
networks:
- ghadir-network
restart: unless-stopped
networks:
ghadir-network:
driver: bridge
volumes:
sqlserver-data: