first commit
This commit is contained in:
76
.gitignore
vendored
Normal file
76
.gitignore
vendored
Normal 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
22
GhadirContest.sln
Normal 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
24
backend/ClientApp/.gitignore
vendored
Normal 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?
|
||||
16
backend/ClientApp/Dockerfile
Normal file
16
backend/ClientApp/Dockerfile
Normal 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;"]
|
||||
73
backend/ClientApp/README.md
Normal file
73
backend/ClientApp/README.md
Normal 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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
22
backend/ClientApp/eslint.config.js
Normal file
22
backend/ClientApp/eslint.config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
])
|
||||
16
backend/ClientApp/index.html
Normal file
16
backend/ClientApp/index.html
Normal 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>
|
||||
38
backend/ClientApp/nginx.conf
Normal file
38
backend/ClientApp/nginx.conf
Normal 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
4763
backend/ClientApp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
backend/ClientApp/package.json
Normal file
46
backend/ClientApp/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
backend/ClientApp/public/favicon.svg
Normal file
1
backend/ClientApp/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
backend/ClientApp/public/icons.svg
Normal file
24
backend/ClientApp/public/icons.svg
Normal 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 |
47
backend/ClientApp/src/App.tsx
Normal file
47
backend/ClientApp/src/App.tsx
Normal 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;
|
||||
BIN
backend/ClientApp/src/assets/hero.png
Normal file
BIN
backend/ClientApp/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
backend/ClientApp/src/assets/react.svg
Normal file
1
backend/ClientApp/src/assets/react.svg
Normal 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 |
1
backend/ClientApp/src/assets/vite.svg
Normal file
1
backend/ClientApp/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
51
backend/ClientApp/src/components/ui/button.tsx
Normal file
51
backend/ClientApp/src/components/ui/button.tsx
Normal 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 }
|
||||
69
backend/ClientApp/src/components/ui/card.tsx
Normal file
69
backend/ClientApp/src/components/ui/card.tsx
Normal 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 }
|
||||
27
backend/ClientApp/src/components/ui/checkbox.tsx
Normal file
27
backend/ClientApp/src/components/ui/checkbox.tsx
Normal 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 }
|
||||
116
backend/ClientApp/src/components/ui/dialog.tsx
Normal file
116
backend/ClientApp/src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
21
backend/ClientApp/src/components/ui/input.tsx
Normal file
21
backend/ClientApp/src/components/ui/input.tsx
Normal 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 }
|
||||
20
backend/ClientApp/src/components/ui/label.tsx
Normal file
20
backend/ClientApp/src/components/ui/label.tsx
Normal 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 }
|
||||
41
backend/ClientApp/src/components/ui/radio-group.tsx
Normal file
41
backend/ClientApp/src/components/ui/radio-group.tsx
Normal 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 }
|
||||
105
backend/ClientApp/src/components/ui/table.tsx
Normal file
105
backend/ClientApp/src/components/ui/table.tsx
Normal 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,
|
||||
}
|
||||
52
backend/ClientApp/src/components/ui/tabs.tsx
Normal file
52
backend/ClientApp/src/components/ui/tabs.tsx
Normal 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 }
|
||||
53
backend/ClientApp/src/context/UserContext.tsx
Normal file
53
backend/ClientApp/src/context/UserContext.tsx
Normal 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;
|
||||
}
|
||||
78
backend/ClientApp/src/index.css
Normal file
78
backend/ClientApp/src/index.css
Normal 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);
|
||||
}
|
||||
}
|
||||
123
backend/ClientApp/src/lib/api.ts
Normal file
123
backend/ClientApp/src/lib/api.ts
Normal 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;
|
||||
6
backend/ClientApp/src/lib/utils.ts
Normal file
6
backend/ClientApp/src/lib/utils.ts
Normal 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))
|
||||
}
|
||||
10
backend/ClientApp/src/main.tsx
Normal file
10
backend/ClientApp/src/main.tsx
Normal 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>,
|
||||
)
|
||||
211
backend/ClientApp/src/pages/AdminPage.tsx
Normal file
211
backend/ClientApp/src/pages/AdminPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
481
backend/ClientApp/src/pages/DashboardPage.tsx
Normal file
481
backend/ClientApp/src/pages/DashboardPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
298
backend/ClientApp/src/pages/LandingPage.tsx
Normal file
298
backend/ClientApp/src/pages/LandingPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
backend/ClientApp/tsconfig.app.json
Normal file
29
backend/ClientApp/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
backend/ClientApp/tsconfig.json
Normal file
7
backend/ClientApp/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
backend/ClientApp/tsconfig.node.json
Normal file
24
backend/ClientApp/tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
27
backend/ClientApp/vite.config.ts
Normal file
27
backend/ClientApp/vite.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
58
backend/Controllers/AdminController.cs
Normal file
58
backend/Controllers/AdminController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
31
backend/Controllers/ConfigController.cs
Normal file
31
backend/Controllers/ConfigController.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
105
backend/Controllers/KhatmController.cs
Normal file
105
backend/Controllers/KhatmController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
165
backend/Controllers/QuizController.cs
Normal file
165
backend/Controllers/QuizController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
backend/Controllers/UserController.cs
Normal file
129
backend/Controllers/UserController.cs
Normal 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
15
backend/DTOs/AdminDtos.cs
Normal 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
27
backend/DTOs/KhatmDtos.cs
Normal 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
45
backend/DTOs/QuizDtos.cs
Normal 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
32
backend/DTOs/UserDtos.cs
Normal 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; }
|
||||
}
|
||||
192
backend/Data/AppDbContext.cs
Normal file
192
backend/Data/AppDbContext.cs
Normal 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
25
backend/Dockerfile
Normal 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"]
|
||||
374
backend/Migrations/20260603125735_InitialCreate.Designer.cs
generated
Normal file
374
backend/Migrations/20260603125735_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
253
backend/Migrations/20260603125735_InitialCreate.cs
Normal file
253
backend/Migrations/20260603125735_InitialCreate.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
381
backend/Migrations/20260603142557_AddKhatmItemUrl.Designer.cs
generated
Normal file
381
backend/Migrations/20260603142557_AddKhatmItemUrl.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
50
backend/Migrations/20260603142557_AddKhatmItemUrl.cs
Normal file
50
backend/Migrations/20260603142557_AddKhatmItemUrl.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
381
backend/Migrations/20260603144334_UpdateTrackingCodeLength.Designer.cs
generated
Normal file
381
backend/Migrations/20260603144334_UpdateTrackingCodeLength.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
451
backend/Migrations/20260603144804_SeedQuizQuestions.Designer.cs
generated
Normal file
451
backend/Migrations/20260603144804_SeedQuizQuestions.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
111
backend/Migrations/20260603144804_SeedQuizQuestions.cs
Normal file
111
backend/Migrations/20260603144804_SeedQuizQuestions.cs
Normal 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, "مسجد النبی", "غدیر خم", "مکه", "مدینه", "خطبه غدیر در کجا ایراد شد؟" });
|
||||
}
|
||||
}
|
||||
}
|
||||
448
backend/Migrations/AppDbContextModelSnapshot.cs
Normal file
448
backend/Migrations/AppDbContextModelSnapshot.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/Models/AppConfig.cs
Normal file
23
backend/Models/AppConfig.cs
Normal 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; }
|
||||
}
|
||||
24
backend/Models/KhatmItem.cs
Normal file
24
backend/Models/KhatmItem.cs
Normal 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>();
|
||||
}
|
||||
37
backend/Models/QuizQuestion.cs
Normal file
37
backend/Models/QuizQuestion.cs
Normal 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
28
backend/Models/User.cs
Normal 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; }
|
||||
}
|
||||
28
backend/Models/UserKhatmSelection.cs
Normal file
28
backend/Models/UserKhatmSelection.cs
Normal 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!;
|
||||
}
|
||||
29
backend/Models/UserQuizAnswer.cs
Normal file
29
backend/Models/UserQuizAnswer.cs
Normal 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!;
|
||||
}
|
||||
28
backend/Models/UserQuizAttempt.cs
Normal file
28
backend/Models/UserQuizAttempt.cs
Normal 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>();
|
||||
}
|
||||
20
backend/Models/UserTotalScore.cs
Normal file
20
backend/Models/UserTotalScore.cs
Normal 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
64
backend/Program.cs
Normal 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();
|
||||
28
backend/Properties/launchSettings.json
Normal file
28
backend/Properties/launchSettings.json
Normal 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
32
backend/appsettings.json
Normal 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
59
backend/backend.csproj
Normal 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
6
backend/backend.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@backend_HostAddress = http://localhost:5004
|
||||
|
||||
GET {{backend_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
47
docker-compose.yml
Normal file
47
docker-compose.yml
Normal 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:
|
||||
Reference in New Issue
Block a user