first commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dr Sousan Website",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["serve", "-p", "3500", "F:/Projects/DrSousan"],
|
||||
"port": 3500
|
||||
},
|
||||
{
|
||||
"name": "Dr Sousan API",
|
||||
"runtimeExecutable": "dotnet",
|
||||
"runtimeArgs": ["run", "-c", "Release", "--no-launch-profile", "--project", "F:/Projects/DrSousan/DrSousan.Api"],
|
||||
"port": 5199
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
**/.git
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/*.user
|
||||
**/*.suo
|
||||
**/node_modules
|
||||
**/*.db
|
||||
**/*.db-journal
|
||||
**/*.db-shm
|
||||
**/*.db-wal
|
||||
**/uploads/*
|
||||
!**/uploads/.gitkeep
|
||||
.env
|
||||
.env.*
|
||||
**/seed_posts.js
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copy this file to .env and fill in your production values.
|
||||
# docker compose reads .env automatically from the project root.
|
||||
|
||||
# Host port (default 5000 → http://localhost:5000)
|
||||
HOST_PORT=5000
|
||||
|
||||
# JWT secret — must be at least 32 characters, keep it secret!
|
||||
JWT_KEY=ReplaceThisWithALongSecureRandomStringAtLeast32Chars!
|
||||
JWT_ISSUER=DrSousanApi
|
||||
JWT_AUDIENCE=DrSousanAdmin
|
||||
|
||||
# Admin panel credentials
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=ChangeThisToAStrongPassword!
|
||||
@@ -0,0 +1,113 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: drsousan-cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HOW THIS WORKS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Runner labels:
|
||||
# ubuntu-latest → container runner ← CI dotnet build runs here
|
||||
# self-hosted → host runner ← docker build/push/deploy runs here
|
||||
#
|
||||
# Local Nexus:
|
||||
# Docker registry → 171.22.25.73:8087 (mcr-proxy + drsousan images)
|
||||
# NuGet → 171.22.25.73:8081/repository/nuget-group/
|
||||
#
|
||||
# Required Gitea secrets:
|
||||
# ENV_FILE → contents of .env
|
||||
# REGISTRY_PASSWORD → Nexus admin password
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
jobs:
|
||||
|
||||
# ── CI: compile-check (runs on every push / PR) ──────────────────────────────
|
||||
ci:
|
||||
name: "CI · dotnet build"
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 171.22.25.73:8087/dotnet/sdk:10.0
|
||||
options: --add-host=gitea:host-gateway
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
git init
|
||||
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||
git config http.extraheader "Authorization: Bearer ${TOKEN}"
|
||||
git fetch --depth=1 origin "${REF}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Restore
|
||||
working-directory: DrSousan.Api
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
run: dotnet restore DrSousan.Api.csproj
|
||||
|
||||
- name: Build
|
||||
working-directory: DrSousan.Api
|
||||
run: dotnet build DrSousan.Api.csproj --no-restore -c Release
|
||||
|
||||
# ── Deploy: build image on server → run (push to main only) ────────────────
|
||||
deploy:
|
||||
name: "Deploy · drsousan"
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
needs: [ci]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
git init
|
||||
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||
git config http.extraheader "Authorization: Bearer ${TOKEN}"
|
||||
git fetch --depth=1 origin "${REF}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Write .env
|
||||
run: printf '%s' "$ENV_FILE" > .env
|
||||
env:
|
||||
ENV_FILE: ${{ secrets.ENV_FILE }}
|
||||
|
||||
- name: Build image
|
||||
run: docker compose build api
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
|
||||
- name: Deploy
|
||||
run: docker compose up -d --no-deps api
|
||||
|
||||
- name: Wait for healthy
|
||||
run: |
|
||||
for i in $(seq 1 24); do
|
||||
STATUS=$(docker inspect --format='{{.State.Health.Status}}' drsousan_api 2>/dev/null || echo "missing")
|
||||
echo " [$i/24] $STATUS"
|
||||
[ "$STATUS" = "healthy" ] && echo "✅ drsousan_api healthy" && exit 0
|
||||
sleep 5
|
||||
done
|
||||
echo "❌ timed out"
|
||||
docker compose logs --tail=60 api
|
||||
exit 1
|
||||
|
||||
- name: Show containers
|
||||
if: always()
|
||||
run: docker compose ps
|
||||
|
||||
- name: Prune old images
|
||||
if: success()
|
||||
run: docker image prune -f
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Secrets
|
||||
.env
|
||||
|
||||
# Build output
|
||||
**/bin/
|
||||
**/obj/
|
||||
|
||||
# SQLite databases
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Uploaded images
|
||||
DrSousan.Api/wwwroot/uploads/*
|
||||
!DrSousan.Api/wwwroot/uploads/.gitkeep
|
||||
|
||||
# IDE
|
||||
.vs/
|
||||
.vscode/
|
||||
*.user
|
||||
*.suo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,152 @@
|
||||
# DrSousan — Deployment Guide
|
||||
|
||||
## Mirrors (Nexus at `171.22.25.73`)
|
||||
|
||||
### Docker
|
||||
Add to Docker Engine config (`/etc/docker/daemon.json` on Linux):
|
||||
```json
|
||||
{
|
||||
"insecure-registries": ["171.22.25.73:8087", "171.22.25.73:8090"]
|
||||
}
|
||||
```
|
||||
Restart Docker, then login:
|
||||
```bash
|
||||
docker login 171.22.25.73:8087 -u admin
|
||||
```
|
||||
|
||||
### NuGet (for local builds)
|
||||
Add nexus source to `NuGet.Config`:
|
||||
```xml
|
||||
<add key="nexus" value="http://171.22.25.73:8081/repository/nuget-group/index.json" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update the Dockerfile to use Nexus mirrors
|
||||
|
||||
Replace the two `FROM` lines in `DrSousan.Api/Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM 171.22.25.73:8090/dotnet/sdk:10.0 AS build
|
||||
...
|
||||
FROM 171.22.25.73:8090/dotnet/aspnet:10.0 AS runtime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## First Deploy (server setup)
|
||||
|
||||
```bash
|
||||
# 1. Copy project to server
|
||||
scp -r . user@server:/opt/drsousan
|
||||
|
||||
# 2. SSH in
|
||||
ssh user@server
|
||||
cd /opt/drsousan
|
||||
|
||||
# 3. Create .env file
|
||||
cp .env.example .env
|
||||
nano .env # fill in JWT_KEY, ADMIN_USERNAME, ADMIN_PASSWORD, HOST_PORT
|
||||
|
||||
# 4. Build and start
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Check it's running:
|
||||
```bash
|
||||
docker compose ps
|
||||
curl http://localhost:5000/healthz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redeploy (update)
|
||||
|
||||
```bash
|
||||
cd /opt/drsousan
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
That's it. SQLite data and uploads are on named volumes — they survive rebuilds.
|
||||
|
||||
---
|
||||
|
||||
## .env file
|
||||
|
||||
```env
|
||||
HOST_PORT=5000
|
||||
JWT_KEY=YourSecretKeyHere32CharsMinimum!!
|
||||
JWT_ISSUER=DrSousanApi
|
||||
JWT_AUDIENCE=DrSousanAdmin
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=YourStrongPassword
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD (GitHub Actions)
|
||||
|
||||
Create `.github/workflows/deploy.yml`:
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build & push image
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 171.22.25.73:8087 -u admin --password-stdin
|
||||
docker build \
|
||||
--build-arg REGISTRY=171.22.25.73:8090 \
|
||||
-t 171.22.25.73:8087/drsousan/api:latest \
|
||||
./DrSousan.Api
|
||||
docker push 171.22.25.73:8087/drsousan/api:latest
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/drsousan
|
||||
docker pull 171.22.25.73:8087/drsousan/api:latest
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**GitHub Secrets to set:**
|
||||
| Secret | Value |
|
||||
|--------|-------|
|
||||
| `REGISTRY_PASSWORD` | Nexus admin password |
|
||||
| `SERVER_HOST` | Server IP |
|
||||
| `SERVER_USER` | SSH user |
|
||||
| `SERVER_SSH_KEY` | Private SSH key |
|
||||
|
||||
For the CI image pull to use Nexus, update `docker-compose.yml` to reference the pre-built image:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: 171.22.25.73:8087/drsousan/api:latest
|
||||
# remove build: section when using CI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
docker compose logs -f api # live logs
|
||||
docker compose restart api # restart without rebuild
|
||||
docker compose down # stop (volumes preserved)
|
||||
docker compose down -v # stop + DELETE all data
|
||||
docker exec -it drsousan_api sh # shell into container
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/*.user
|
||||
**/*.db
|
||||
**/*.db-shm
|
||||
**/*.db-wal
|
||||
**/uploads
|
||||
**/node_modules
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/Dockerfile*
|
||||
**/*.md
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Data;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<SiteSetting> SiteSettings => Set<SiteSetting>();
|
||||
public DbSet<Service> Services => Set<Service>();
|
||||
public DbSet<GalleryItem> GalleryItems => Set<GalleryItem>();
|
||||
public DbSet<Testimonial> Testimonials => Set<Testimonial>();
|
||||
public DbSet<BlogCategory> BlogCategories => Set<BlogCategory>();
|
||||
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder mb)
|
||||
{
|
||||
mb.Entity<SiteSetting>()
|
||||
.HasIndex(s => new { s.Section, s.Key })
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<BlogPost>()
|
||||
.HasIndex(b => b.Slug)
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<BlogCategory>()
|
||||
.HasIndex(c => c.Slug)
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<Comment>()
|
||||
.HasOne(c => c.Parent)
|
||||
.WithMany(c => c.Replies)
|
||||
.HasForeignKey(c => c.ParentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# ── Stage 1: Build ────────────────────────────────────────────────────────────
|
||||
FROM 171.22.25.73:8087/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Restore dependencies first (layer-cache friendly)
|
||||
COPY DrSousan.Api.csproj NuGet.Config ./
|
||||
RUN ok=0; \
|
||||
for i in 1 2 3 4 5; do \
|
||||
dotnet restore DrSousan.Api.csproj --configfile NuGet.Config && ok=1 && break; \
|
||||
echo "Restore attempt $i failed, retrying in 30s..."; sleep 30; \
|
||||
done; \
|
||||
[ "$ok" = "1" ]
|
||||
|
||||
# Copy everything and publish
|
||||
COPY . .
|
||||
RUN dotnet publish DrSousan.Api.csproj \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--no-restore
|
||||
|
||||
# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
|
||||
FROM 171.22.25.73:8087/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Create directories for persistent volumes and set ownership
|
||||
# .NET 10 aspnet image ships with a built-in non-root user "app" (uid 1654)
|
||||
RUN mkdir -p /data /app/wwwroot/uploads
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# /data → SQLite DB (mount as named volume)
|
||||
# /app/wwwroot/uploads → uploaded images (mount as named volume)
|
||||
VOLUME ["/data", "/app/wwwroot/uploads"]
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
# Self-probe via the app's own runtime — the aspnet image has no curl/wget.
|
||||
HEALTHCHECK --interval=15s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD ["dotnet", "DrSousan.Api.dll", "--healthcheck"]
|
||||
|
||||
ENTRYPOINT ["dotnet", "DrSousan.Api.dll"]
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RazorLangVersion>latest</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@DrSousan.Api_HostAddress = http://localhost:5199
|
||||
|
||||
GET {{DrSousan.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DrSousan.Api.Models;
|
||||
|
||||
// ─── Site Settings (key-value store per section) ─────────────────────────────
|
||||
public class SiteSetting
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
[MaxLength(50)] public string Section { get; set; } = "";
|
||||
}
|
||||
|
||||
// ─── Service card ─────────────────────────────────────────────────────────────
|
||||
public class Service
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(200)] public string Title { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
[MaxLength(500)] public string IconSvg { get; set; } = "";
|
||||
[MaxLength(500)] public string BeforeImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string AfterImageUrl { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Gallery item ─────────────────────────────────────────────────────────────
|
||||
public class GalleryItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(500)] public string ImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string BeforeImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string AfterImageUrl { get; set; } = "";
|
||||
[MaxLength(100)] public string Category { get; set; } = "";
|
||||
[MaxLength(300)] public string Caption { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Testimonial ──────────────────────────────────────────────────────────────
|
||||
public class Testimonial
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string AuthorName { get; set; } = "";
|
||||
[MaxLength(10)] public string AuthorEmoji { get; set; } = "👩";
|
||||
public string Text { get; set; } = "";
|
||||
public int Rating { get; set; } = 5;
|
||||
[MaxLength(50)] public string Date { get; set; } = "";
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Blog category ────────────────────────────────────────────────────────────
|
||||
public class BlogCategory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string Name { get; set; } = "";
|
||||
[MaxLength(120)] public string Slug { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public ICollection<BlogPost> Posts { get; set; } = new List<BlogPost>();
|
||||
}
|
||||
|
||||
// ─── Blog post ────────────────────────────────────────────────────────────────
|
||||
public class BlogPost
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Content
|
||||
[MaxLength(300)] public string Title { get; set; } = "";
|
||||
[MaxLength(320)] public string Slug { get; set; } = "";
|
||||
public string Excerpt { get; set; } = "";
|
||||
public string Content { get; set; } = "";
|
||||
[MaxLength(500)] public string FeaturedImage { get; set; } = "";
|
||||
[MaxLength(100)] public string Author { get; set; } = "دکتر سوسن آلطه";
|
||||
|
||||
// SEO
|
||||
[MaxLength(70)] public string MetaTitle { get; set; } = "";
|
||||
[MaxLength(160)] public string MetaDescription { get; set; } = "";
|
||||
[MaxLength(100)] public string FocusKeyword { get; set; } = "";
|
||||
public string Keywords { get; set; } = ""; // comma-separated
|
||||
[MaxLength(500)] public string OgImage { get; set; } = "";
|
||||
|
||||
// Schema.org
|
||||
[MaxLength(50)] public string ArticleType { get; set; } = "MedicalWebPage";
|
||||
|
||||
// Status
|
||||
public bool IsPublished { get; set; } = false;
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Metrics
|
||||
public int ViewCount { get; set; } = 0;
|
||||
public int ReadingTimeMinutes { get; set; } = 5;
|
||||
|
||||
// Relations
|
||||
public int? CategoryId { get; set; }
|
||||
public BlogCategory? Category { get; set; }
|
||||
}
|
||||
|
||||
// ─── Blog comment ────────────────────────────────────────────────────────────
|
||||
public class Comment
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string AuthorName { get; set; } = "";
|
||||
[MaxLength(200)] public string AuthorEmail { get; set; } = "";
|
||||
public string Body { get; set; } = "";
|
||||
public bool IsApproved { get; set; } = false;
|
||||
public bool IsAdminReply { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
// Post relation
|
||||
public int BlogPostId { get; set; }
|
||||
public BlogPost? Post { get; set; }
|
||||
// Self-referential reply
|
||||
public int? ParentId { get; set; }
|
||||
public Comment? Parent { get; set; }
|
||||
public ICollection<Comment> Replies { get; set; } = new List<Comment>();
|
||||
}
|
||||
|
||||
// ─── FAQ ─────────────────────────────────────────────────────────────────────
|
||||
public class Faq
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Question { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────────────
|
||||
public record LoginRequest(string Username, string Password);
|
||||
public record ChangePasswordRequest(string CurrentPassword, string NewPassword);
|
||||
public record SettingDto(string Key, string Value);
|
||||
public record BulkSettingsDto(Dictionary<string, string> Settings);
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org"
|
||||
value="https://api.nuget.org/v3/index.json"
|
||||
protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nexus"
|
||||
value="http://171.22.25.73:8081/repository/nuget-group/index.json"
|
||||
protocolVersion="3"
|
||||
allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,116 @@
|
||||
@page "/blog"
|
||||
@model DrSousan.Api.Pages.Blog.BlogIndexModel
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="مقالات تخصصی دکتر سوسن آلطه درباره زیبایی پوست، بوتاکس، فیلر، لیزر و مراقبت از پوست." />
|
||||
<link rel="canonical" href="@(Request.Scheme + "://" + Request.Host + "/blog")" />
|
||||
<style>
|
||||
/* ─── Blog Hero ─────────────────────────────────────────────── */
|
||||
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
||||
.blog-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
||||
.blog-hero p{font-size:1rem;color:var(--mid);max-width:520px;margin:0 auto}
|
||||
/* ─── Search ─────────────────────────────────────────────────── */
|
||||
.search-wrap{max-width:480px;margin:1.5rem auto 0;position:relative}
|
||||
.search-wrap input{width:100%;border:1.5px solid var(--border);border-radius:50px;padding:.65rem 1.2rem .65rem 3rem;font-family:'Vazirmatn',sans-serif;font-size:.9rem;direction:rtl;outline:none;background:var(--white);transition:border-color .2s}
|
||||
.search-wrap input:focus{border-color:var(--gold)}
|
||||
.search-wrap svg{position:absolute;left:1rem;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--light)}
|
||||
/* ─── Filter ─────────────────────────────────────────────────── */
|
||||
.filter-bar{max-width:1100px;margin:2rem auto 0;padding:0 2rem;display:flex;gap:.6rem;flex-wrap:wrap}
|
||||
.filter-btn{background:transparent;border:1.5px solid var(--border);color:var(--mid);padding:.4rem 1.1rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.85rem;cursor:pointer;transition:all .2s;text-decoration:none;display:inline-block}
|
||||
.filter-btn.active,.filter-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
|
||||
/* ─── Blog Grid ──────────────────────────────────────────────── */
|
||||
.blog-grid{max-width:1100px;margin:2rem auto;padding:0 2rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem}
|
||||
@@media(max-width:900px){.blog-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@@media(max-width:600px){.blog-grid{grid-template-columns:1fr}}
|
||||
.post-card{background:var(--white);border-radius:16px;border:1px solid var(--border);overflow:hidden;transition:transform .3s,box-shadow .3s;display:flex;flex-direction:column}
|
||||
.post-card:hover{transform:translateY(-4px);box-shadow:0 12px 40px rgba(184,149,90,.15)}
|
||||
.post-card-img{aspect-ratio:16/9;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:2rem;overflow:hidden}
|
||||
.post-card-img img{width:100%;height:100%;object-fit:cover}
|
||||
.post-card-body{padding:1.3rem;flex:1;display:flex;flex-direction:column;gap:.6rem}
|
||||
.post-cat{font-size:.72rem;font-weight:600;color:var(--gold);background:var(--gold-pale);padding:.2rem .7rem;border-radius:50px;display:inline-block}
|
||||
.post-title{font-size:1rem;font-weight:600;color:var(--dark);line-height:1.5}
|
||||
.post-title:hover{color:var(--gold)}
|
||||
.post-excerpt{font-size:.85rem;color:var(--mid);line-height:1.7;flex:1}
|
||||
.post-meta{display:flex;align-items:center;justify-content:space-between;font-size:.75rem;color:var(--light);margin-top:auto;padding-top:.6rem;border-top:1px solid var(--border)}
|
||||
.read-more{color:var(--gold);font-weight:500;font-size:.82rem}
|
||||
/* ─── Empty ──────────────────────────────────────────────────── */
|
||||
.empty{text-align:center;padding:4rem 2rem;color:var(--light);grid-column:1/-1}
|
||||
/* ─── Pagination ─────────────────────────────────────────────── */
|
||||
.pagination{display:flex;gap:.5rem;justify-content:center;padding:2rem;margin-top:1rem;max-width:1100px;margin-left:auto;margin-right:auto}
|
||||
.page-btn{width:38px;height:38px;border-radius:8px;border:1.5px solid var(--border);background:transparent;font-family:'Vazirmatn',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;text-decoration:none;color:var(--dark);font-size:.9rem}
|
||||
.page-btn.active,.page-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="blog-hero">
|
||||
<h1>وبلاگ تخصصی پوست و زیبایی</h1>
|
||||
<p>آخرین مقالات و راهنماهای تخصصی درباره مراقبت از پوست، زیبایی و درمانهای تخصصی</p>
|
||||
<div class="search-wrap">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="searchInput" placeholder="جستجو در مقالات..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<a href="/blog" class="filter-btn @(string.IsNullOrEmpty(Model.ActiveCat) ? "active" : "")">همه</a>
|
||||
@foreach (var cat in Model.Categories)
|
||||
{
|
||||
var active = Model.ActiveCat == cat.Slug;
|
||||
var count = cat.Posts.Count(p => p.IsPublished);
|
||||
<a href="/blog?category=@cat.Slug" class="filter-btn @(active ? "active" : "")">@cat.Name (@count)</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="blog-grid" id="blogGrid">
|
||||
@if (!Model.Posts.Any())
|
||||
{
|
||||
<div class="empty"><p>مقالهای یافت نشد.</p></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<div class="post-card">
|
||||
<div class="post-card-img">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
<div class="post-card-body">
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="post-cat">@post.Category.Name</span>
|
||||
}
|
||||
<a href="/blog/@post.Slug" class="post-title">@post.Title</a>
|
||||
<p class="post-excerpt">@(post.Excerpt.Length > 120 ? post.Excerpt.Substring(0, 120) + "..." : post.Excerpt)</p>
|
||||
<div class="post-meta">
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه | 👁 @post.ViewCount</span>
|
||||
<a href="/blog/@post.Slug" class="read-more">ادامه مطلب ←</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.TotalPages > 1)
|
||||
{
|
||||
<div class="pagination">
|
||||
@if (Model.CurrentPage > 1)
|
||||
{
|
||||
<a class="page-btn" href="/blog?page=@(Model.CurrentPage - 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">‹</a>
|
||||
}
|
||||
@for (int p = 1; p <= Model.TotalPages; p++)
|
||||
{
|
||||
<a class="page-btn @(p == Model.CurrentPage ? "active" : "")"
|
||||
href="/blog?page=@p@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">@p</a>
|
||||
}
|
||||
@if (Model.CurrentPage < Model.TotalPages)
|
||||
{
|
||||
<a class="page-btn" href="/blog?page=@(Model.CurrentPage + 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">›</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages.Blog;
|
||||
|
||||
public class BlogIndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private const int PageSize = 10;
|
||||
|
||||
public BlogIndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<BlogPost> Posts { get; private set; } = new();
|
||||
public List<BlogCategory> Categories { get; private set; } = new();
|
||||
public int CurrentPage { get; private set; } = 1;
|
||||
public int TotalPages { get; private set; } = 1;
|
||||
public int TotalPosts { get; private set; } = 0;
|
||||
public string? ActiveCat { get; private set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int page = 1, string? category = null)
|
||||
{
|
||||
CurrentPage = page < 1 ? 1 : page;
|
||||
ActiveCat = category;
|
||||
|
||||
var q = _db.BlogPosts.Include(p => p.Category).Where(p => p.IsPublished);
|
||||
|
||||
if (!string.IsNullOrEmpty(category))
|
||||
q = q.Where(p => p.Category != null && p.Category.Slug == category);
|
||||
|
||||
TotalPosts = await q.CountAsync();
|
||||
TotalPages = Math.Max(1, (int)Math.Ceiling(TotalPosts / (double)PageSize));
|
||||
|
||||
if (CurrentPage > TotalPages) CurrentPage = TotalPages;
|
||||
|
||||
Posts = await q
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
Categories = await _db.BlogCategories
|
||||
.Include(c => c.Posts)
|
||||
.ToListAsync();
|
||||
|
||||
ViewData["SiteName"] = await GetSiteNameAsync();
|
||||
ViewData["Title"] = "وبلاگ | دکتر سوسن آلطه";
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<string> GetSiteNameAsync()
|
||||
{
|
||||
var s = await _db.SiteSettings
|
||||
.FirstOrDefaultAsync(x => x.Section == "hero" && x.Key == "name");
|
||||
return s?.Value ?? "دکتر سوسن آلطه";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
@page "/blog/{slug}"
|
||||
@model DrSousan.Api.Pages.Blog.PostModel
|
||||
@{
|
||||
var post = Model.Post!;
|
||||
var baseUrl = Request.Scheme + "://" + Request.Host;
|
||||
var canonicalUrl = baseUrl + "/blog/" + post.Slug;
|
||||
var ogImage = ViewData["OgImage"]?.ToString() ?? "";
|
||||
var articleType = ViewData["ArticleType"]?.ToString() ?? "MedicalWebPage";
|
||||
var pubDate = post.PublishedAt?.ToString("yyyy-MM-ddTHH:mm:ssZ") ?? "";
|
||||
var updDate = post.UpdatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="@ViewData["MetaDesc"]" />
|
||||
@if (!string.IsNullOrEmpty(ViewData["Keywords"]?.ToString())) {
|
||||
<meta name="keywords" content="@ViewData["Keywords"]" />
|
||||
}
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content="@ViewData["Title"]" />
|
||||
<meta property="og:description" content="@ViewData["MetaDesc"]" />
|
||||
<meta property="og:url" content="@canonicalUrl" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||
<meta property="og:image" content="@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)" />
|
||||
}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="@ViewData["Title"]" />
|
||||
<meta name="twitter:description" content="@ViewData["MetaDesc"]" />
|
||||
<link rel="canonical" href="@canonicalUrl" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "@articleType",
|
||||
"headline": "@post.Title.Replace("\"","\\\"") ",
|
||||
"description": "@(ViewData["MetaDesc"]?.ToString()?.Replace("\"","\\\""))",
|
||||
"author": { "@@type": "Person", "name": "@post.Author" },
|
||||
"publisher": {
|
||||
"@@type": "Organization",
|
||||
"name": "@ViewData["SiteName"]",
|
||||
"url": "@baseUrl"
|
||||
},
|
||||
"datePublished": "@pubDate",
|
||||
"dateModified": "@updDate",
|
||||
"mainEntityOfPage": "@canonicalUrl"
|
||||
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||
@:,"image": "@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ─── Post Layout ──────────────────────────────────────────────── */
|
||||
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
||||
@@media(max-width:900px){.post-layout{grid-template-columns:1fr;padding-top:5rem}}
|
||||
/* ─── Article ──────────────────────────────────────────────────── */
|
||||
.article-hero{border-radius:16px;overflow:hidden;margin-bottom:2rem;aspect-ratio:16/6;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;font-size:3rem}
|
||||
.article-hero img{width:100%;height:100%;object-fit:cover}
|
||||
.article-cat{display:inline-block;background:var(--gold-pale);color:var(--gold);font-size:.75rem;font-weight:600;padding:.25rem .8rem;border-radius:50px;margin-bottom:.8rem}
|
||||
.article-title{font-size:clamp(1.5rem,3vw,2rem);font-weight:700;line-height:1.4;margin-bottom:1rem}
|
||||
.article-meta{display:flex;gap:1.5rem;font-size:.8rem;color:var(--light);margin-bottom:2rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||||
.article-meta span{display:flex;align-items:center;gap:.3rem}
|
||||
/* ─── Content ──────────────────────────────────────────────────── */
|
||||
.article-content{font-size:.95rem;line-height:2;color:var(--dark)}
|
||||
.article-content h2{font-size:1.3rem;font-weight:700;margin:1.8rem 0 .8rem;color:var(--dark);padding-bottom:.4rem;border-bottom:2px solid var(--gold-pale)}
|
||||
.article-content h3{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .6rem;color:var(--dark)}
|
||||
.article-content p{margin-bottom:1rem}
|
||||
.article-content ul,.article-content ol{padding-right:1.5rem;margin-bottom:1rem}
|
||||
.article-content li{margin-bottom:.4rem}
|
||||
.article-content strong{color:var(--dark);font-weight:600}
|
||||
.article-content a{color:var(--gold);border-bottom:1px solid var(--gold-pale)}
|
||||
.article-content blockquote{border-right:4px solid var(--gold);padding:.8rem 1.2rem;background:var(--gold-pale);border-radius:0 8px 8px 0;margin:1.2rem 0;font-style:italic;color:var(--mid)}
|
||||
/* ─── Tags ─────────────────────────────────────────────────────── */
|
||||
.article-tags{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--border);display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.tag{background:var(--bg);border:1px solid var(--border);padding:.25rem .75rem;border-radius:50px;font-size:.78rem;color:var(--mid)}
|
||||
/* ─── Share ─────────────────────────────────────────────────────── */
|
||||
.share-box{margin-top:2rem;padding:1.5rem;background:var(--white);border-radius:12px;border:1px solid var(--border);text-align:center}
|
||||
.share-title{font-size:.9rem;font-weight:600;margin-bottom:1rem}
|
||||
.share-btns{display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
|
||||
.share-btn{padding:.45rem 1rem;border-radius:8px;font-family:'Vazirmatn',sans-serif;font-size:.82rem;cursor:pointer;border:none;font-weight:500}
|
||||
.share-telegram{background:#2CA5E0;color:#fff}
|
||||
.share-whatsapp{background:#25D366;color:#fff}
|
||||
.share-copy{background:var(--bg);color:var(--mid);border:1px solid var(--border)}
|
||||
/* ─── CTA ──────────────────────────────────────────────────────── */
|
||||
.cta-box{margin-top:2.5rem;padding:2rem;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);border-radius:16px;text-align:center}
|
||||
.cta-box h3{font-size:1.1rem;font-weight:700;margin-bottom:.6rem}
|
||||
.cta-box p{font-size:.88rem;color:var(--mid);margin-bottom:1.2rem}
|
||||
.cta-btn{background:var(--gold);color:#fff;padding:.7rem 1.8rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.9rem;font-weight:600;border:none;cursor:pointer;display:inline-block;text-decoration:none}
|
||||
/* ─── Sidebar ──────────────────────────────────────────────────── */
|
||||
.sidebar{position:sticky;top:90px}
|
||||
.sidebar-card{background:var(--white);border-radius:14px;border:1px solid var(--border);padding:1.4rem;margin-bottom:1.5rem}
|
||||
.sidebar-title{font-size:.88rem;font-weight:700;color:var(--dark);margin-bottom:1rem;padding-bottom:.6rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post{display:flex;gap:.8rem;margin-bottom:.9rem;padding-bottom:.9rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}
|
||||
.recent-img{width:56px;height:56px;border-radius:8px;background:var(--gold-pale);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:1.3rem;overflow:hidden}
|
||||
.recent-img img{width:100%;height:100%;object-fit:cover;border-radius:8px}
|
||||
.recent-title{font-size:.82rem;font-weight:600;line-height:1.4;color:var(--dark);text-decoration:none}
|
||||
.recent-title:hover{color:var(--gold)}
|
||||
.recent-date{font-size:.73rem;color:var(--light);margin-top:.2rem}
|
||||
.doctor-card{text-align:center}
|
||||
.doc-avatar{width:80px;height:80px;border-radius:50%;background:var(--gold-pale);margin:0 auto .8rem;display:flex;align-items:center;justify-content:center;font-size:2rem}
|
||||
.doc-name{font-size:.95rem;font-weight:700;color:var(--dark)}
|
||||
.doc-title{font-size:.78rem;color:var(--light);margin:.2rem 0 .8rem}
|
||||
.doc-btn{background:var(--gold);color:#fff;padding:.5rem 1.2rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.82rem;border:none;cursor:pointer;width:100%;text-decoration:none;display:block;text-align:center}
|
||||
/* ─── Comments ─────────────────────────────────────────────────── */
|
||||
.comments-section{margin-top:3rem}
|
||||
.comments-section h2{font-size:1.3rem;font-weight:700;margin-bottom:1.5rem;color:var(--dark)}
|
||||
.comment-card{background:var(--gold-pale);border-radius:12px;padding:1.2rem;margin-bottom:1rem;opacity:.98}
|
||||
.comment-card.admin-reply{background:rgba(184,149,90,0.12);border-right:3px solid var(--gold)}
|
||||
.comment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||
.comment-author{font-weight:600;font-size:.92rem;color:var(--dark)}
|
||||
.comment-author.admin{color:var(--gold)}
|
||||
.comment-date{font-size:.78rem;color:var(--light)}
|
||||
.comment-body{font-size:.88rem;color:var(--mid);line-height:1.8}
|
||||
.comment-replies{margin-top:1rem;margin-right:1.5rem}
|
||||
.comment-form-wrap{margin-top:2rem;background:var(--section-bg);border-radius:16px;padding:1.75rem;border:1px solid var(--border)}
|
||||
.comment-form-wrap h3{font-size:1rem;font-weight:700;margin-bottom:1.2rem;color:var(--dark)}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.form-group{display:flex;flex-direction:column;gap:.4rem;margin-bottom:1rem}
|
||||
.form-group label{font-size:.85rem;font-weight:500;color:var(--dark)}
|
||||
.form-group input,.form-group textarea{background:var(--white);border:1.5px solid var(--border);border-radius:10px;padding:.65rem .9rem;font-family:'Vazirmatn',sans-serif;font-size:.88rem;color:var(--dark);direction:rtl;outline:none;transition:border-color .2s}
|
||||
.form-group input:focus,.form-group textarea:focus{border-color:var(--gold)}
|
||||
.form-group textarea{resize:vertical;min-height:100px}
|
||||
.form-success{display:none;background:#e6f4ea;color:#1e7e34;border-radius:8px;padding:.8rem 1rem;text-align:center;margin-bottom:1rem;font-size:.88rem}
|
||||
.form-success.show{display:block}
|
||||
@@media(max-width:600px){.form-row{grid-template-columns:1fr}}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="post-layout">
|
||||
<!-- ── Article ── -->
|
||||
<article>
|
||||
<div class="article-hero">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="article-cat">@post.Category.Name</span>
|
||||
}
|
||||
<h1 class="article-title">@post.Title</h1>
|
||||
<div class="article-meta">
|
||||
<span>✍️ @post.Author</span>
|
||||
@if (post.PublishedAt.HasValue)
|
||||
{
|
||||
<span>📅 @post.PublishedAt.Value.ToString("yyyy/MM/dd")</span>
|
||||
}
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه مطالعه</span>
|
||||
<span>👁 @post.ViewCount بازدید</span>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
@Html.Raw(post.Content)
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(post.Keywords))
|
||||
{
|
||||
<div class="article-tags">
|
||||
@foreach (var kw in post.Keywords.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
<span class="tag">@kw.Trim()</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="share-box">
|
||||
<div class="share-title">این مقاله را به اشتراک بگذارید</div>
|
||||
<div class="share-btns">
|
||||
<a class="share-btn share-telegram" href="https://t.me/share/url?url=@Uri.EscapeDataString(canonicalUrl)&text=@Uri.EscapeDataString(post.Title)" target="_blank" rel="noopener">📱 تلگرام</a>
|
||||
<a class="share-btn share-whatsapp" href="https://wa.me/?text=@Uri.EscapeDataString(post.Title + " " + canonicalUrl)" target="_blank" rel="noopener">💬 واتساپ</a>
|
||||
<button class="share-btn share-copy" onclick="navigator.clipboard.writeText('@canonicalUrl').then(()=>{this.textContent='✓ کپی شد!';setTimeout(()=>this.textContent='🔗 کپی لینک',2000)})">🔗 کپی لینک</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<h3>آماده تحول در پوستتان هستید؟</h3>
|
||||
<p>همین امروز با دکتر سوسن آلطه مشاوره رایگان دریافت کنید</p>
|
||||
<a href="/#contact" class="cta-btn">رزرو نوبت رایگان</a>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<section class="comments-section" id="comments">
|
||||
<h2>نظرات (@Model.Comments.Count)</h2>
|
||||
|
||||
@if (!Model.Comments.Any())
|
||||
{
|
||||
<p style="color:var(--light);margin-bottom:2rem;">اولین نظر را ثبت کنید!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var comment in Model.Comments)
|
||||
{
|
||||
<div class="comment-card @(comment.IsAdminReply ? "admin-reply" : "")">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author @(comment.IsAdminReply ? "admin" : "")">
|
||||
@(comment.IsAdminReply ? "👩⚕️ " : "")@comment.AuthorName
|
||||
</span>
|
||||
<span class="comment-date">@comment.CreatedAt.ToString("yyyy/MM/dd")</span>
|
||||
</div>
|
||||
<p class="comment-body">@comment.Body</p>
|
||||
@if (comment.Replies.Any())
|
||||
{
|
||||
<div class="comment-replies">
|
||||
@foreach (var reply in comment.Replies)
|
||||
{
|
||||
<div class="comment-card admin-reply">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author admin">👩⚕️ @reply.AuthorName</span>
|
||||
<span class="comment-date">@reply.CreatedAt.ToString("yyyy/MM/dd")</span>
|
||||
</div>
|
||||
<p class="comment-body">@reply.Body</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="comment-form-wrap">
|
||||
<h3>ثبت نظر</h3>
|
||||
@if (Model.CommentSent)
|
||||
{
|
||||
<div class="form-success show">✅ نظر شما ثبت شد و پس از تأیید نمایش داده میشود.</div>
|
||||
}
|
||||
<form method="post" asp-page="/Blog/Post" asp-route-slug="@post.Slug">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentAuthor">نام *</label>
|
||||
<input asp-for="CommentAuthor" placeholder="نام شما" />
|
||||
<span asp-validation-for="CommentAuthor" style="color:red;font-size:.78rem;"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentEmail">ایمیل (اختیاری)</label>
|
||||
<input asp-for="CommentEmail" placeholder="ایمیل شما" dir="ltr" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentBody">نظر *</label>
|
||||
<textarea asp-for="CommentBody" placeholder="نظر خود را بنویسید..."></textarea>
|
||||
<span asp-validation-for="CommentBody" style="color:red;font-size:.78rem;"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">ارسال نظر</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<!-- ── Sidebar ── -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-card doctor-card">
|
||||
<div class="doc-avatar">👩⚕️</div>
|
||||
<div class="doc-name">@post.Author</div>
|
||||
<div class="doc-title">پزشک عمومی | متخصص زیبایی پوست</div>
|
||||
<a href="/#contact" class="doc-btn">رزرو نوبت</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">دستهبندی</div>
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<a href="/blog?category=@post.Category.Slug" style="color:var(--gold);font-size:.9rem;">@post.Category.Name ←</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(post.FocusKeyword))
|
||||
{
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">کلیدواژه اصلی</div>
|
||||
<span style="background:var(--gold-pale);padding:.3rem .7rem;border-radius:12px;font-size:.82rem;color:var(--mid);">@post.FocusKeyword</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">خدمات ما</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.5rem;font-size:.85rem">
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> بوتاکس و فیلر</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> لیزر درمانی</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مزوتراپی</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> پاکسازی پوست</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مشاوره زیبایی</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,153 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages.Blog;
|
||||
|
||||
public class PostModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public PostModel(AppDbContext db) => _db = db;
|
||||
|
||||
public BlogPost? Post { get; private set; }
|
||||
public List<CommentVm> Comments { get; private set; } = new();
|
||||
public bool CommentSent { get; private set; } = false;
|
||||
|
||||
// Comment form binding
|
||||
[BindProperty]
|
||||
[Required(ErrorMessage = "نام الزامی است.")]
|
||||
[MaxLength(100)]
|
||||
public string CommentAuthor { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
[MaxLength(200)]
|
||||
[EmailAddress]
|
||||
public string CommentEmail { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
[Required(ErrorMessage = "متن نظر الزامی است.")]
|
||||
public string CommentBody { get; set; } = "";
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string slug)
|
||||
{
|
||||
var post = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.FirstOrDefaultAsync(p => p.Slug == slug && p.IsPublished);
|
||||
|
||||
if (post is null) return NotFound();
|
||||
|
||||
// Increment view count
|
||||
post.ViewCount++;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Post = post;
|
||||
await LoadCommentsAsync(post.Id);
|
||||
await SetViewDataAsync(post);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string slug)
|
||||
{
|
||||
var post = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.FirstOrDefaultAsync(p => p.Slug == slug && p.IsPublished);
|
||||
|
||||
if (post is null) return NotFound();
|
||||
|
||||
Post = post;
|
||||
await LoadCommentsAsync(post.Id);
|
||||
await SetViewDataAsync(post);
|
||||
|
||||
if (!ModelState.IsValid) return Page();
|
||||
|
||||
// Sanitise
|
||||
var body = System.Text.RegularExpressions.Regex.Replace(CommentBody ?? "", "<[^>]*>", "").Trim();
|
||||
var authorName = System.Text.RegularExpressions.Regex.Replace(CommentAuthor ?? "", "<[^>]*>", "").Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body) || string.IsNullOrWhiteSpace(authorName))
|
||||
{
|
||||
ModelState.AddModelError("", "نام و متن نظر الزامی است.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
_db.Comments.Add(new Comment
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
AuthorName = authorName,
|
||||
AuthorEmail = CommentEmail ?? "",
|
||||
Body = body,
|
||||
IsApproved = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
CommentSent = true;
|
||||
// Clear form
|
||||
CommentAuthor = "";
|
||||
CommentEmail = "";
|
||||
CommentBody = "";
|
||||
ModelState.Clear();
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task LoadCommentsAsync(int postId)
|
||||
{
|
||||
var raw = await _db.Comments
|
||||
.Where(c => c.BlogPostId == postId && c.IsApproved && c.ParentId == null)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.Select(c => new
|
||||
{
|
||||
c.Id, c.AuthorName, c.Body, c.CreatedAt, c.IsAdminReply,
|
||||
Replies = _db.Comments
|
||||
.Where(r => r.ParentId == c.Id && r.IsApproved)
|
||||
.OrderBy(r => r.CreatedAt)
|
||||
.Select(r => new { r.Id, r.AuthorName, r.Body, r.CreatedAt, r.IsAdminReply })
|
||||
.ToList()
|
||||
}).ToListAsync();
|
||||
|
||||
Comments = raw.Select(c => new CommentVm
|
||||
{
|
||||
Id = c.Id, AuthorName = c.AuthorName, Body = c.Body,
|
||||
CreatedAt = c.CreatedAt, IsAdminReply = c.IsAdminReply,
|
||||
Replies = c.Replies.Select(r => new CommentVm
|
||||
{
|
||||
Id = r.Id, AuthorName = r.AuthorName, Body = r.Body,
|
||||
CreatedAt = r.CreatedAt, IsAdminReply = r.IsAdminReply
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task SetViewDataAsync(BlogPost post)
|
||||
{
|
||||
var metaTitle = string.IsNullOrEmpty(post.MetaTitle) ? post.Title : post.MetaTitle;
|
||||
var metaDesc = string.IsNullOrEmpty(post.MetaDescription) ? post.Excerpt : post.MetaDescription;
|
||||
|
||||
// Avoid duplicating the site name if MetaTitle already contains it
|
||||
var suffix = " | دکتر سوسن آلطه";
|
||||
ViewData["Title"] = metaTitle.Contains("دکتر سوسن") ? metaTitle : metaTitle + suffix;
|
||||
ViewData["MetaDesc"] = metaDesc;
|
||||
ViewData["Keywords"] = post.Keywords;
|
||||
ViewData["OgImage"] = string.IsNullOrEmpty(post.OgImage) ? post.FeaturedImage : post.OgImage;
|
||||
ViewData["ArticleType"] = post.ArticleType;
|
||||
ViewData["Slug"] = post.Slug;
|
||||
|
||||
var s = await _db.SiteSettings
|
||||
.FirstOrDefaultAsync(x => x.Section == "hero" && x.Key == "name");
|
||||
ViewData["SiteName"] = s?.Value ?? "دکتر سوسن آلطه";
|
||||
}
|
||||
|
||||
// View model for comments
|
||||
public class CommentVm
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string AuthorName { get; set; } = "";
|
||||
public string Body { get; set; } = "";
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool IsAdminReply { get; set; }
|
||||
public List<CommentVm> Replies { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@using DrSousan.Api.Models
|
||||
@using DrSousan.Api.Data
|
||||
@namespace DrSousan.Api.Pages.Blog
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,742 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
var h = Model.Hero;
|
||||
var a = Model.About;
|
||||
var c = Model.Contact;
|
||||
var siteName = h.GetValueOrDefault("name", "دکتر سوسن آلطه");
|
||||
var heroImg = h.GetValueOrDefault("image", "");
|
||||
var badgeHidden = h.GetValueOrDefault("badge_hidden", "") == "true";
|
||||
var badgeTitle = h.TryGetValue("badge_title", out var _bt) && !string.IsNullOrWhiteSpace(_bt) ? _bt : "متخصص پوست و زیبایی";
|
||||
var badgeSub = h.TryGetValue("badge_subtitle", out var _bs) && !string.IsNullOrWhiteSpace(_bs) ? _bs : "فارغالتحصیل دانشگاه ایران";
|
||||
var aboutImg = a.GetValueOrDefault("image", "");
|
||||
var yearsExp = a.GetValueOrDefault("years_exp","۱۰+");
|
||||
var ig = c.GetValueOrDefault("instagram","");
|
||||
var wa = c.GetValueOrDefault("whatsapp","");
|
||||
var tg = c.GetValueOrDefault("telegram","");
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="@h.GetValueOrDefault("subtitle","")" />
|
||||
<meta name="keywords" content="دکتر پوست تهران,بوتاکس,لیزر موهای زائد,فیلر,مزوتراپی,زیبایی پوست" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="@ViewData["Title"]" />
|
||||
<meta property="og:description" content="@h.GetValueOrDefault("subtitle","")" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
<link rel="canonical" href="@(Request.Scheme + "://" + Request.Host + "/")" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":["MedicalBusiness","LocalBusiness"],
|
||||
"name":"@siteName",
|
||||
"description":"@h.GetValueOrDefault("subtitle","")",
|
||||
"url":"@(Request.Scheme + "://" + Request.Host)",
|
||||
"telephone":"@c.GetValueOrDefault("phone","")",
|
||||
"address":{"@@type":"PostalAddress","addressLocality":"تهران","addressCountry":"IR","streetAddress":"@c.GetValueOrDefault("address","")"},
|
||||
"openingHours":"@c.GetValueOrDefault("hours","")",
|
||||
"medicalSpecialty":"Dermatology"
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* ─── Hero ─────────────────────────────────────────────────── */
|
||||
#hero { min-height:100svh; display:flex; align-items:center; padding:100px 0 3rem; position:relative; overflow:hidden; }
|
||||
#hero::before { content:''; position:absolute; top:-120px; left:-120px; width:600px; height:600px; background:radial-gradient(circle, rgba(184,149,90,0.10) 0%, transparent 70%); pointer-events:none; }
|
||||
#hero::after { content:''; position:absolute; bottom:-80px; right:-80px; width:500px; height:500px; background:radial-gradient(circle, rgba(184,149,90,0.07) 0%, transparent 70%); pointer-events:none; }
|
||||
.hero-inner { max-width:1200px; margin:0 auto; padding:0 2rem; display:grid; grid-template-columns:1fr 1fr; gap:4rem; align-items:center; }
|
||||
.hero-text { animation: fadeUp 0.9s ease both; }
|
||||
.hero-tag { display:inline-block; background:var(--gold-pale); color:var(--gold); font-size:0.78rem; font-weight:500; padding:0.35rem 1rem; border-radius:50px; margin-bottom:1.4rem; letter-spacing:0.05em; }
|
||||
.hero-name { font-size:clamp(2.2rem,4vw,3.2rem); font-weight:700; line-height:1.3; color:var(--dark); margin-bottom:0.6rem; }
|
||||
.hero-name span { color:var(--gold); }
|
||||
.hero-subtitle { font-size:1.05rem; color:var(--mid); font-weight:400; margin-bottom:1.8rem; line-height:1.9; }
|
||||
.hero-stats { display:flex; gap:2.5rem; margin-bottom:2.5rem; }
|
||||
.stat-item { text-align:center; }
|
||||
.stat-number { font-size:2rem; font-weight:700; color:var(--gold); display:block; line-height:1; }
|
||||
.stat-label { font-size:0.78rem; color:var(--light); margin-top:0.3rem; }
|
||||
.hero-actions { display:flex; gap:1rem; flex-wrap:wrap; }
|
||||
.hero-image { position:relative; animation:fadeUp 0.9s 0.2s ease both; }
|
||||
.hero-image-frame { width:100%; aspect-ratio:4/5; border-radius:30% 70% 70% 30% / 30% 30% 70% 70%; background:linear-gradient(145deg, var(--gold-pale) 0%, #EFE3CC 100%); position:relative; overflow:hidden; box-shadow:0 30px 80px rgba(184,149,90,0.2); }
|
||||
.hero-image-frame img { width:100%; height:100%; object-fit:cover; }
|
||||
.hero-image-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1rem; color:var(--gold); }
|
||||
.hero-image-placeholder svg { width:80px; height:80px; opacity:0.5; }
|
||||
.hero-image-placeholder p { font-size:0.85rem; opacity:0.6; }
|
||||
.hero-badge { position:absolute; bottom:2rem; left:-2rem; background:var(--white); border-radius:16px; padding:1rem 1.4rem; box-shadow:0 10px 40px rgba(0,0,0,0.1); display:flex; align-items:center; gap:0.8rem; animation:float 3s ease-in-out infinite; }
|
||||
.badge-icon { width:42px; height:42px; background:var(--gold-pale); border-radius:50%; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.badge-icon svg { width:20px; height:20px; color:var(--gold); }
|
||||
.badge-text { font-size:0.82rem; line-height:1.4; }
|
||||
.badge-text strong { color:var(--gold); display:block; font-size:0.9rem; }
|
||||
/* ─── About ─────────────────────────────────────────────────── */
|
||||
#about { background:var(--white); }
|
||||
.about-grid { display:grid; grid-template-columns:1fr 1fr; gap:5rem; align-items:center; }
|
||||
.about-image-wrap { position:relative; }
|
||||
.about-img-box { width:100%; aspect-ratio:3/4; border-radius:20px; background:linear-gradient(160deg, var(--gold-pale) 0%, #EDE0CA 100%); overflow:hidden; position:relative; }
|
||||
.about-img-box img { width:100%; height:100%; object-fit:cover; }
|
||||
.about-img-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1rem; color:var(--gold); opacity:0.5; }
|
||||
.about-img-placeholder svg { width:60px; height:60px; }
|
||||
.about-accent-box { position:absolute; bottom:-2rem; right:-2rem; background:var(--gold); color:var(--white); border-radius:16px; padding:1.5rem 2rem; text-align:center; }
|
||||
.about-accent-box .big { font-size:2.2rem; font-weight:700; display:block; }
|
||||
.about-accent-box .small { font-size:0.82rem; opacity:0.85; }
|
||||
.credential-list { list-style:none; display:flex; flex-direction:column; gap:1rem; margin-top:2rem; }
|
||||
.credential-list li { display:flex; align-items:flex-start; gap:0.85rem; font-size:0.95rem; color:var(--mid); line-height:1.6; }
|
||||
.cred-icon { width:28px; height:28px; border-radius:50%; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:0.1rem; }
|
||||
.cred-icon svg { width:13px; height:13px; color:var(--gold); }
|
||||
/* ─── Services ─────────────────────────────────────────────── */
|
||||
#services { background:var(--section-bg); }
|
||||
.services-header { text-align:center; margin-bottom:3.5rem; }
|
||||
.services-header .divider { margin:1.2rem auto 0; }
|
||||
.services-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.service-card { background:var(--white); border-radius:20px; padding:2rem; border:1px solid var(--border); transition:transform 0.3s, box-shadow 0.3s, border-color 0.3s; position:relative; overflow:hidden; }
|
||||
.service-card::before { content:''; position:absolute; top:0; right:0; width:80px; height:80px; background:radial-gradient(circle at top right, rgba(184,149,90,0.08), transparent 70%); }
|
||||
.service-card:hover { transform:translateY(-6px); box-shadow:0 20px 50px rgba(184,149,90,0.15); border-color:var(--gold-pale); }
|
||||
.service-icon { width:56px; height:56px; background:var(--gold-pale); border-radius:16px; display:flex; align-items:center; justify-content:center; margin-bottom:1.3rem; }
|
||||
.service-icon svg { width:26px; height:26px; color:var(--gold); }
|
||||
.service-title { font-size:1.05rem; font-weight:600; color:var(--dark); margin-bottom:0.6rem; }
|
||||
.service-desc { font-size:0.88rem; color:var(--mid); line-height:1.8; }
|
||||
/* ─── Service Before/After ────────────────────────────────── */
|
||||
.svc-ba { margin-top:1.2rem; border-radius:12px; overflow:hidden; position:relative; aspect-ratio:4/3; cursor:pointer; user-select:none; }
|
||||
.svc-ba img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transition:opacity 0.45s ease; }
|
||||
.svc-ba .ba-after { opacity:0; }
|
||||
.svc-ba.show-after .ba-before { opacity:0; }
|
||||
.svc-ba.show-after .ba-after { opacity:1; }
|
||||
.svc-ba-btns { position:absolute; bottom:0; left:0; right:0; display:flex; z-index:2; }
|
||||
.svc-ba-btn { flex:1; padding:0.45rem 0; text-align:center; font-size:0.78rem; font-weight:700; font-family:'Vazirmatn',sans-serif; border:none; cursor:pointer; transition:all 0.25s; background:rgba(0,0,0,0.42); color:rgba(255,255,255,0.75); letter-spacing:0.03em; }
|
||||
.svc-ba-btn.active { background:var(--gold); color:#fff; }
|
||||
.svc-ba-only { margin-top:1.2rem; border-radius:12px; overflow:hidden; position:relative; aspect-ratio:4/3; }
|
||||
.svc-ba-only img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||
.svc-ba-only .ba-solo-label { position:absolute; bottom:0; left:0; right:0; padding:0.4rem; text-align:center; font-size:0.78rem; font-weight:700; font-family:'Vazirmatn',sans-serif; background:var(--gold); color:#fff; }
|
||||
/* ─── Gallery ──────────────────────────────────────────────── */
|
||||
#gallery { background:var(--white); }
|
||||
.gallery-header { margin-bottom:3rem; }
|
||||
.gallery-tabs { display:flex; gap:0.6rem; margin-bottom:2.5rem; flex-wrap:wrap; }
|
||||
.tab-btn { background:transparent; border:1.5px solid var(--border); color:var(--mid); padding:0.45rem 1.2rem; border-radius:50px; font-family:'Vazirmatn',sans-serif; font-size:0.85rem; cursor:pointer; transition:all 0.25s; }
|
||||
.tab-btn.active, .tab-btn:hover { background:var(--gold); border-color:var(--gold); color:var(--white); }
|
||||
.gallery-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.2rem; }
|
||||
.gallery-item { border-radius:16px; overflow:hidden; aspect-ratio:4/3; background:linear-gradient(135deg, var(--gold-pale), #EDE0CA); position:relative; cursor:pointer; }
|
||||
.gallery-item img { width:100%; height:100%; object-fit:cover; transition:transform 0.4s; }
|
||||
.gallery-item:hover img { transform:scale(1.05); }
|
||||
.gallery-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0.6rem; color:var(--gold); opacity:0.45; }
|
||||
.gallery-placeholder svg { width:36px; height:36px; }
|
||||
.gallery-placeholder p { font-size:0.75rem; }
|
||||
.gallery-item-overlay { position:absolute; inset:0; background:rgba(184,149,90,0); display:flex; align-items:center; justify-content:center; transition:background 0.3s; }
|
||||
.gallery-item:hover .gallery-item-overlay { background:rgba(184,149,90,0.15); }
|
||||
/* ─── Testimonials ─────────────────────────────────────────── */
|
||||
#testimonials { background:var(--section-bg); }
|
||||
.testimonials-header { text-align:center; margin-bottom:3rem; }
|
||||
.testimonials-header .divider { margin:1rem auto 0; }
|
||||
.testimonials-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.testimonial-card { background:var(--white); border-radius:20px; padding:2rem; border:1px solid var(--border); position:relative; }
|
||||
.testimonial-card::before { content:'"'; position:absolute; top:1rem; left:1.5rem; font-size:4rem; color:var(--gold-pale); line-height:1; font-family:Georgia,serif; }
|
||||
.testimonial-stars { display:flex; gap:3px; margin-bottom:1rem; }
|
||||
.star { color:var(--gold); font-size:1rem; }
|
||||
.testimonial-text { font-size:0.9rem; color:var(--mid); line-height:1.9; margin-bottom:1.5rem; }
|
||||
.testimonial-author { display:flex; align-items:center; gap:0.8rem; }
|
||||
.author-avatar { width:42px; height:42px; border-radius:50%; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; font-size:1.1rem; flex-shrink:0; }
|
||||
.author-name { font-size:0.88rem; font-weight:600; color:var(--dark); }
|
||||
.author-date { font-size:0.78rem; color:var(--light); }
|
||||
/* ─── Blog Preview ─────────────────────────────────────────── */
|
||||
#blog-preview { background:var(--white); }
|
||||
.blog-preview-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.post-card-prev { background:var(--bg); border-radius:16px; border:1px solid var(--border); overflow:hidden; transition:transform 0.3s,box-shadow 0.3s; display:flex; flex-direction:column; }
|
||||
.post-card-prev:hover { transform:translateY(-4px); box-shadow:0 12px 40px rgba(184,149,90,0.15); }
|
||||
.post-card-prev-img { aspect-ratio:16/9; background:linear-gradient(135deg,var(--gold-pale),#EDE0CA); display:flex; align-items:center; justify-content:center; color:var(--gold); font-size:2rem; overflow:hidden; }
|
||||
.post-card-prev-img img { width:100%; height:100%; object-fit:cover; }
|
||||
.post-card-prev-body { padding:1.3rem; flex:1; display:flex; flex-direction:column; gap:0.6rem; }
|
||||
.post-cat-label { font-size:0.72rem; font-weight:600; color:var(--gold); background:var(--gold-pale); padding:0.2rem 0.7rem; border-radius:50px; display:inline-block; }
|
||||
.post-card-prev-title { font-size:1rem; font-weight:600; color:var(--dark); line-height:1.5; }
|
||||
.post-card-prev-title:hover { color:var(--gold); }
|
||||
.post-card-prev-excerpt { font-size:0.85rem; color:var(--mid); line-height:1.7; flex:1; }
|
||||
.post-card-prev-meta { display:flex; align-items:center; justify-content:space-between; font-size:0.75rem; color:var(--light); margin-top:auto; padding-top:0.6rem; border-top:1px solid var(--border); }
|
||||
.read-more-link { color:var(--gold); font-weight:500; font-size:0.82rem; }
|
||||
.blog-preview-more { text-align:center; margin-top:2.5rem; }
|
||||
/* ─── FAQ ──────────────────────────────────────────────────── */
|
||||
#faq { background:var(--section-bg); }
|
||||
.faq-list { max-width:780px; margin:0 auto; display:flex; flex-direction:column; gap:0.8rem; }
|
||||
.faq-item { background:var(--white); border-radius:12px; border:1px solid var(--border); overflow:hidden; }
|
||||
.faq-item summary { padding:1.25rem 1.5rem; cursor:pointer; font-weight:600; list-style:none; display:flex; justify-content:space-between; align-items:center; color:var(--dark); font-size:0.95rem; }
|
||||
.faq-item summary::-webkit-details-marker { display:none; }
|
||||
.faq-item summary::after { content:"+"; color:var(--gold); font-size:1.1rem; flex-shrink:0; margin-right:1rem; }
|
||||
.faq-item[open] summary::after { content:"-"; }
|
||||
.faq-answer { padding:0 1.5rem 1.25rem; color:var(--mid); font-size:0.92rem; line-height:1.85; border-top:1px solid var(--border); }
|
||||
/* ─── Contact ──────────────────────────────────────────────── */
|
||||
#contact { background:var(--white); }
|
||||
.contact-grid { display:grid; grid-template-columns:1fr 1.4fr; gap:4rem; align-items:start; }
|
||||
.contact-info-list { display:flex; flex-direction:column; gap:1.5rem; margin-top:2rem; }
|
||||
.contact-info-item { display:flex; align-items:flex-start; gap:1rem; }
|
||||
.info-icon { width:48px; height:48px; background:var(--gold-pale); border-radius:14px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.info-icon svg { width:22px; height:22px; color:var(--gold); }
|
||||
.info-text strong { display:block; font-size:0.9rem; color:var(--dark); margin-bottom:0.2rem; }
|
||||
.info-text p { font-size:0.88rem; color:var(--mid); line-height:1.6; }
|
||||
.social-links { display:flex; gap:0.8rem; margin-top:2.5rem; }
|
||||
.social-link { width:44px; height:44px; border-radius:12px; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; color:var(--gold); text-decoration:none; transition:background 0.25s, transform 0.2s; }
|
||||
.social-link:hover { background:var(--gold); color:var(--white); transform:translateY(-3px); }
|
||||
.social-link svg { width:20px; height:20px; }
|
||||
.contact-form { background:var(--section-bg); border-radius:24px; padding:2.5rem; border:1px solid var(--border); }
|
||||
.form-title { font-size:1.2rem; font-weight:600; color:var(--dark); margin-bottom:0.5rem; }
|
||||
.form-sub { font-size:0.88rem; color:var(--mid); margin-bottom:2rem; }
|
||||
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
|
||||
.form-group { display:flex; flex-direction:column; gap:0.4rem; margin-bottom:1.2rem; }
|
||||
.form-group label { font-size:0.85rem; font-weight:500; color:var(--dark); }
|
||||
.form-group input, .form-group select, .form-group textarea { background:var(--white); border:1.5px solid var(--border); border-radius:12px; padding:0.75rem 1rem; font-family:'Vazirmatn',sans-serif; font-size:0.9rem; color:var(--dark); direction:rtl; transition:border-color 0.25s, box-shadow 0.25s; outline:none; }
|
||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:var(--gold); box-shadow:0 0 0 3px rgba(184,149,90,0.12); }
|
||||
.form-group textarea { resize:vertical; min-height:110px; }
|
||||
.form-submit { width:100%; background:var(--gold); color:var(--white); border:none; padding:0.9rem; border-radius:12px; font-family:'Vazirmatn',sans-serif; font-size:0.95rem; font-weight:600; cursor:pointer; transition:background 0.25s, transform 0.2s; }
|
||||
.form-submit:hover { background:var(--gold-light); transform:translateY(-2px); }
|
||||
/* ─── Responsive ───────────────────────────────────────────── */
|
||||
@@media (max-width:900px) {
|
||||
.hero-inner { grid-template-columns:1fr; text-align:center; gap:2.5rem; }
|
||||
.hero-text { order:1; }
|
||||
.hero-image { order:2; max-width:320px; margin:0 auto; }
|
||||
.hero-stats { justify-content:center; gap:2rem; }
|
||||
.hero-actions { justify-content:center; flex-wrap:wrap; gap:.8rem; }
|
||||
.hero-badge { left:0; right:0; margin:0 auto; width:fit-content; }
|
||||
.about-grid { grid-template-columns:1fr; gap:3rem; }
|
||||
.about-accent-box { right:0; bottom:-1rem; }
|
||||
.services-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.gallery-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.testimonials-grid { grid-template-columns:1fr; }
|
||||
.contact-grid { grid-template-columns:1fr; gap:2.5rem; }
|
||||
.blog-preview-grid { grid-template-columns:repeat(2,1fr); }
|
||||
}
|
||||
@@media (max-width:600px) {
|
||||
section { padding:3.5rem 0; }
|
||||
.container { padding:0 1.2rem; }
|
||||
.hero-inner { padding:0 1.2rem; gap:2rem; }
|
||||
.hero-image { max-width:260px; }
|
||||
.hero-stats { gap:1.5rem; }
|
||||
.hero-stats .stat-number { font-size:1.6rem; }
|
||||
.hero-badge { position:static; margin-top:1rem; width:auto; border-radius:12px; padding:.7rem 1rem; }
|
||||
.services-grid { grid-template-columns:1fr; }
|
||||
.gallery-grid { grid-template-columns:repeat(2,1fr); gap:.8rem; }
|
||||
.blog-preview-grid { grid-template-columns:1fr; }
|
||||
.form-row { grid-template-columns:1fr; }
|
||||
.about-accent-box { position:static; border-radius:12px; margin-top:1.5rem; display:inline-block; }
|
||||
.about-image-wrap { text-align:center; }
|
||||
.testimonials-grid { grid-template-columns:1fr; }
|
||||
.section-title { font-size:1.5rem; }
|
||||
.faq-item summary { font-size:.9rem; padding:1rem 1.2rem; }
|
||||
.faq-answer { padding:0 1.2rem 1rem; font-size:.88rem; }
|
||||
}
|
||||
@@media (max-width:380px) {
|
||||
.gallery-grid { grid-template-columns:1fr; }
|
||||
.hero-actions .btn-primary,
|
||||
.hero-actions .btn-secondary { width:100%; text-align:center; }
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- ══════ HERO ══════ -->
|
||||
<section id="hero">
|
||||
<div class="hero-inner">
|
||||
<div class="hero-text">
|
||||
<span class="hero-tag">@h.GetValueOrDefault("tag","پزشک عمومی و متخصص زیبایی پوست")</span>
|
||||
<h1 class="hero-name">@h.GetValueOrDefault("name_line1","دکتر") <span>@h.GetValueOrDefault("name_line2","سوسن")</span><br>@h.GetValueOrDefault("name_line3","آلطه")</h1>
|
||||
<p class="hero-subtitle">@h.GetValueOrDefault("subtitle","با بیش از یک دهه تجربه در حوزهی زیبایی و مراقبت از پوست، زیبایی واقعی شما را با علم و هنر همراه میکنیم.")</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat1_num","+۱۰")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat1_lbl","سال تجربه")</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat2_num","+۵۰۰")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat2_lbl","بیمار راضی")</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat3_num","+۱۵")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat3_lbl","خدمات تخصصی")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a href="#contact" class="btn-primary">@h.GetValueOrDefault("cta_primary","رزرو نوبت")</a>
|
||||
<a href="#about" class="btn-secondary">@h.GetValueOrDefault("cta_secondary","بیشتر بدانید")</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-image">
|
||||
<div class="hero-image-frame">
|
||||
@if (!string.IsNullOrEmpty(heroImg))
|
||||
{
|
||||
<img src="@heroImg" alt="@siteName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="hero-image-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<p>تصویر دکتر</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!badgeHidden)
|
||||
{
|
||||
<div class="hero-badge">
|
||||
<div class="badge-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="badge-text">
|
||||
<strong>@badgeTitle</strong>
|
||||
@badgeSub
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ ABOUT ══════ -->
|
||||
<section id="about">
|
||||
<div class="container">
|
||||
<div class="about-grid">
|
||||
<div class="about-image-wrap fade-in">
|
||||
<div class="about-img-box">
|
||||
@if (!string.IsNullOrEmpty(aboutImg))
|
||||
{
|
||||
<img src="@aboutImg" alt="@siteName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="about-img-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="about-accent-box">
|
||||
<span class="big">@yearsExp</span>
|
||||
<span class="small">سال تجربه</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fade-in">
|
||||
<span class="section-label">درباره من</span>
|
||||
<h2 class="section-title">@a.GetValueOrDefault("title","سلامت و زیبایی پوست<br>هدف اصلی من است")</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">@a.GetValueOrDefault("bio","دکتر سوسن آلطه، پزشک عمومی و متخصص زیبایی پوست با سالها تجربه در ارائه خدمات پیشرفته پوست و زیبایی.")</p>
|
||||
<ul class="credential-list">
|
||||
@foreach (var key in new[] { "cred1","cred2","cred3","cred4","cred5" })
|
||||
{
|
||||
var val = a.GetValueOrDefault(key, "");
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
<li>
|
||||
<div class="cred-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>@val</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ SERVICES ══════ -->
|
||||
<section id="services">
|
||||
<div class="container">
|
||||
<div class="services-header fade-in">
|
||||
<span class="section-label">خدمات ما</span>
|
||||
<h2 class="section-title">طیف گستردهای از خدمات<br>زیبایی و مراقبت از پوست</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="services-grid">
|
||||
@foreach (var svc in Model.Services)
|
||||
{
|
||||
var hasBefore = !string.IsNullOrEmpty(svc.BeforeImageUrl);
|
||||
var hasAfter = !string.IsNullOrEmpty(svc.AfterImageUrl);
|
||||
<div class="service-card fade-in">
|
||||
<div class="service-icon">
|
||||
@if (!string.IsNullOrEmpty(svc.IconSvg))
|
||||
{
|
||||
@Html.Raw(svc.IconSvg)
|
||||
}
|
||||
else
|
||||
{
|
||||
@Html.Raw(IndexModel.ServiceIconSvg(svc.Title))
|
||||
}
|
||||
</div>
|
||||
<div class="service-title">@svc.Title</div>
|
||||
<p class="service-desc">@svc.Description</p>
|
||||
@if (hasBefore && hasAfter)
|
||||
{
|
||||
<div class="svc-ba" onclick="toggleBA(this)" title="برای مشاهده قبل/بعد کلیک کنید">
|
||||
<img class="ba-before" src="@svc.BeforeImageUrl" alt="قبل از درمان @svc.Title" loading="lazy"/>
|
||||
<img class="ba-after" src="@svc.AfterImageUrl" alt="بعد از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="svc-ba-btns">
|
||||
<button class="svc-ba-btn active" type="button">قبل</button>
|
||||
<button class="svc-ba-btn" type="button">بعد</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (hasBefore)
|
||||
{
|
||||
<div class="svc-ba-only">
|
||||
<img src="@svc.BeforeImageUrl" alt="قبل از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="ba-solo-label">قبل از درمان</div>
|
||||
</div>
|
||||
}
|
||||
else if (hasAfter)
|
||||
{
|
||||
<div class="svc-ba-only">
|
||||
<img src="@svc.AfterImageUrl" alt="بعد از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="ba-solo-label">بعد از درمان</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ GALLERY ══════ -->
|
||||
<section id="gallery">
|
||||
<div class="container">
|
||||
<div class="gallery-header fade-in">
|
||||
<span class="section-label">گالری</span>
|
||||
<h2 class="section-title">نتایج قبل و بعد</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">نمونهای از نتایج فوقالعاده درمانهای انجامشده توسط دکتر آلطه.</p>
|
||||
</div>
|
||||
<div class="gallery-tabs fade-in">
|
||||
<button class="tab-btn active">همه</button>
|
||||
<button class="tab-btn">بوتاکس</button>
|
||||
<button class="tab-btn">لیزر</button>
|
||||
<button class="tab-btn">مزوتراپی</button>
|
||||
<button class="tab-btn">پاکسازی</button>
|
||||
</div>
|
||||
<div class="gallery-grid">
|
||||
@if (Model.Gallery.Any())
|
||||
{
|
||||
@foreach (var item in Model.Gallery)
|
||||
{
|
||||
<div class="gallery-item fade-in">
|
||||
@if (!string.IsNullOrEmpty(item.ImageUrl))
|
||||
{
|
||||
<img src="@item.ImageUrl" alt="@item.Caption" loading="lazy" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="gallery-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
<p>تصویر قبل و بعد</p>
|
||||
</div>
|
||||
}
|
||||
<div class="gallery-item-overlay"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (int i = 0; i < 6; i++)
|
||||
{
|
||||
<div class="gallery-item fade-in">
|
||||
<div class="gallery-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
<p>تصویر قبل و بعد</p>
|
||||
</div>
|
||||
<div class="gallery-item-overlay"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ TESTIMONIALS ══════ -->
|
||||
<section id="testimonials">
|
||||
<div class="container">
|
||||
<div class="testimonials-header fade-in">
|
||||
<span class="section-label">نظرات بیماران</span>
|
||||
<h2 class="section-title">بیماران ما چه میگویند</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="testimonials-grid">
|
||||
@foreach (var t in Model.Testimonials)
|
||||
{
|
||||
<div class="testimonial-card fade-in">
|
||||
<div class="testimonial-stars">
|
||||
@for (int i = 0; i < (t.Rating > 0 ? t.Rating : 5); i++)
|
||||
{
|
||||
<span class="star">★</span>
|
||||
}
|
||||
</div>
|
||||
<p class="testimonial-text">@t.Text</p>
|
||||
<div class="testimonial-author">
|
||||
<div class="author-avatar">@(string.IsNullOrEmpty(t.AuthorEmoji) ? "👩" : t.AuthorEmoji)</div>
|
||||
<div>
|
||||
<div class="author-name">@t.AuthorName</div>
|
||||
<div class="author-date">@t.Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ BLOG PREVIEW ══════ -->
|
||||
@if (Model.RecentPosts.Any())
|
||||
{
|
||||
<section id="blog-preview">
|
||||
<div class="container">
|
||||
<div class="fade-in" style="margin-bottom:3.5rem;">
|
||||
<span class="section-label">وبلاگ</span>
|
||||
<h2 class="section-title">آخرین مقالات</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">دانش و تجربه در حوزه زیبایی پوست</p>
|
||||
</div>
|
||||
<div class="blog-preview-grid">
|
||||
@foreach (var post in Model.RecentPosts)
|
||||
{
|
||||
<div class="post-card-prev fade-in">
|
||||
<div class="post-card-prev-img">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
<div class="post-card-prev-body">
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="post-cat-label">@post.Category.Name</span>
|
||||
}
|
||||
<a href="/blog/@post.Slug" class="post-card-prev-title">@post.Title</a>
|
||||
<p class="post-card-prev-excerpt">@(post.Excerpt.Length > 120 ? post.Excerpt.Substring(0, 120) + "..." : post.Excerpt)</p>
|
||||
<div class="post-card-prev-meta">
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه | 👁 @post.ViewCount</span>
|
||||
<a href="/blog/@post.Slug" class="read-more-link">ادامه مطلب ←</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="blog-preview-more">
|
||||
<a href="/blog" class="btn-secondary">مشاهده همه مقالات</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- ══════ FAQ ══════ -->
|
||||
<section id="faq">
|
||||
<div class="container">
|
||||
<div class="fade-in" style="margin-bottom:3rem;">
|
||||
<span class="section-label">سوالات متداول</span>
|
||||
<h2 class="section-title">پاسخ به پرسشهای رایج</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="faq-list">
|
||||
@foreach (var faq in Model.Faqs)
|
||||
{
|
||||
<details class="faq-item">
|
||||
<summary>@faq.Question</summary>
|
||||
<div class="faq-answer">@faq.Answer</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ CONTACT ══════ -->
|
||||
<section id="contact">
|
||||
<div class="container">
|
||||
<div class="contact-grid">
|
||||
<div class="fade-in">
|
||||
<span class="section-label">تماس با ما</span>
|
||||
<h2 class="section-title">رزرو نوبت<br>و مشاوره</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">برای رزرو نوبت یا دریافت مشاوره رایگان با ما در تماس باشید.</p>
|
||||
<div class="contact-info-list">
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("phone","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>تلفن تماس</strong>
|
||||
<p>@c.GetValueOrDefault("phone","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("email","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>ایمیل</strong>
|
||||
<p>@c.GetValueOrDefault("email","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("address","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>آدرس مطب</strong>
|
||||
<p>@c.GetValueOrDefault("address","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("hours","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>ساعات کاری</strong>
|
||||
<p>@c.GetValueOrDefault("hours","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="social-links">
|
||||
@if (!string.IsNullOrEmpty(ig))
|
||||
{
|
||||
<a href="@ig" class="social-link" target="_blank" rel="noopener" title="اینستاگرام">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"/>
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/>
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(wa))
|
||||
{
|
||||
<a href="@wa" class="social-link" target="_blank" rel="noopener" title="واتساپ">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(tg))
|
||||
{
|
||||
<a href="@tg" class="social-link" target="_blank" rel="noopener" title="تلگرام">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-form fade-in">
|
||||
<div class="form-title">رزرو نوبت آنلاین</div>
|
||||
<p class="form-sub">فرم زیر را پر کنید، در اسرع وقت با شما تماس میگیریم.</p>
|
||||
<form onsubmit="handleSubmit(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>نام</label>
|
||||
<input type="text" placeholder="نام شما" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>نام خانوادگی</label>
|
||||
<input type="text" placeholder="نام خانوادگی" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>شماره موبایل</label>
|
||||
<input type="tel" placeholder="09XX-XXX-XXXX" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>نوع خدمت مورد نظر</label>
|
||||
<select>
|
||||
<option value="" disabled selected>انتخاب خدمت</option>
|
||||
@foreach (var svc in Model.Services)
|
||||
{
|
||||
<option>@svc.Title</option>
|
||||
}
|
||||
<option>سایر</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>توضیحات (اختیاری)</label>
|
||||
<textarea placeholder="مشکل پوستی یا سوالات خود را اینجا بنویسید..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="form-submit">ارسال و رزرو نوبت</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Before/After toggle
|
||||
function toggleBA(wrap) {
|
||||
wrap.classList.toggle('show-after');
|
||||
const btns = wrap.querySelectorAll('.svc-ba-btn');
|
||||
btns[0].classList.toggle('active');
|
||||
btns[1].classList.toggle('active');
|
||||
}
|
||||
|
||||
// Tab buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('.form-submit');
|
||||
btn.textContent = '✓ درخواست شما ثبت شد';
|
||||
btn.style.background = '#2D7A4F';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'ارسال و رزرو نوبت';
|
||||
btn.style.background = '';
|
||||
e.target.reset();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Active nav link on scroll
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollY = window.scrollY;
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop - 100;
|
||||
const id = section.getAttribute('id');
|
||||
const link = document.querySelector(`header nav a[href="#${id}"]`);
|
||||
if (link && scrollY > sectionTop && scrollY <= sectionTop + section.offsetHeight) {
|
||||
document.querySelectorAll('header nav a').forEach(a => a.style.color = '');
|
||||
link.style.color = '#B8955A';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
// Hero
|
||||
public Dictionary<string, string> Hero { get; private set; } = new();
|
||||
public Dictionary<string, string> About { get; private set; } = new();
|
||||
public Dictionary<string, string> Contact { get; private set; } = new();
|
||||
|
||||
// Collections
|
||||
public List<Service> Services { get; private set; } = new();
|
||||
public List<GalleryItem> Gallery { get; private set; } = new();
|
||||
public List<Testimonial> Testimonials { get; private set; } = new();
|
||||
public List<BlogPost> RecentPosts { get; private set; } = new();
|
||||
public List<Faq> Faqs { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var settings = await _db.SiteSettings.ToListAsync();
|
||||
|
||||
Hero = settings.Where(s => s.Section == "hero")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
About = settings.Where(s => s.Section == "about")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
Contact = settings.Where(s => s.Section == "contact")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
|
||||
Services = await _db.Services
|
||||
.Where(s => s.IsActive)
|
||||
.OrderBy(s => s.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Gallery = await _db.GalleryItems
|
||||
.Where(g => g.IsActive)
|
||||
.OrderBy(g => g.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Testimonials = await _db.Testimonials
|
||||
.Where(t => t.IsActive)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
RecentPosts = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.Where(p => p.IsPublished)
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.Take(3)
|
||||
.ToListAsync();
|
||||
|
||||
Faqs = await _db.Faqs
|
||||
.Where(f => f.IsActive)
|
||||
.OrderBy(f => f.Order)
|
||||
.ToListAsync();
|
||||
|
||||
// Expose site name for layout
|
||||
var siteName = Hero.GetValueOrDefault("name", "دکتر سوسن آلطه");
|
||||
ViewData["SiteName"] = siteName;
|
||||
ViewData["Title"] = $"{siteName} | متخصص زیبایی پوست تهران";
|
||||
}
|
||||
|
||||
// ── Helpers for the view ─────────────────────────────────────────────────
|
||||
public static string Stars(int count)
|
||||
{
|
||||
var s = string.Concat(Enumerable.Repeat("⭐", count));
|
||||
return string.IsNullOrEmpty(s) ? "⭐⭐⭐⭐⭐" : s;
|
||||
}
|
||||
|
||||
public static string ServiceIcon(string title) => title switch
|
||||
{
|
||||
var t when t.Contains("لیزر") => "✨",
|
||||
var t when t.Contains("بوتاکس") || t.Contains("فیلر") => "💉",
|
||||
var t when t.Contains("مزو") => "💊",
|
||||
var t when t.Contains("پاکسازی") => "🧴",
|
||||
var t when t.Contains("مشاوره") => "🩺",
|
||||
var t when t.Contains("هایفو") || t.Contains("HIFU") => "🔬",
|
||||
var t when t.Contains("RF") => "⚡",
|
||||
_ => "🌟"
|
||||
};
|
||||
|
||||
public static string ServiceIconSvg(string title) => title switch
|
||||
{
|
||||
var t when t.Contains("لیزر") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>""",
|
||||
var t when t.Contains("بوتاکس") || t.Contains("فیلر") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>""",
|
||||
var t when t.Contains("مزو") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>""",
|
||||
var t when t.Contains("پاکسازی") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>""",
|
||||
var t when t.Contains("مشاوره") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>""",
|
||||
_ =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>"""
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@RenderSection("Head", required: false)
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--gold: #B8955A;
|
||||
--gold-light: #D4B483;
|
||||
--gold-pale: #F5ECD8;
|
||||
--bg: #FAFAF7;
|
||||
--white: #FFFFFF;
|
||||
--dark: #1E1E1E;
|
||||
--mid: #5A5A5A;
|
||||
--light: #9A9A9A;
|
||||
--border: #E8E2D9;
|
||||
--section-bg: #F7F4EF;
|
||||
}
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: 'Vazirmatn', sans-serif; background: var(--bg); color: var(--dark); line-height: 1.8; direction: rtl; }
|
||||
a { text-decoration: none; color: inherit; }
|
||||
/* Navbar */
|
||||
header {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(250,250,247,0.92); backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border); padding: 0 2rem;
|
||||
height: 70px; display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.logo { font-size: 1rem; font-weight: 600; color: var(--gold); letter-spacing: 0.03em; text-decoration: none; }
|
||||
nav { display: flex; gap: 2rem; list-style: none; }
|
||||
nav a { text-decoration: none; color: var(--mid); font-size: 0.88rem; font-weight: 400; transition: color 0.25s; position: relative; }
|
||||
nav a::after { content: ''; position: absolute; bottom: -3px; right: 0; width: 0; height: 1px; background: var(--gold); transition: width 0.3s; }
|
||||
nav a:hover { color: var(--gold); }
|
||||
nav a:hover::after { width: 100%; }
|
||||
.nav-cta { background: var(--gold); color: var(--white) !important; padding: 0.45rem 1.2rem; border-radius: 50px; font-size: 0.85rem !important; transition: background 0.25s, transform 0.2s !important; }
|
||||
.nav-cta:hover { background: var(--gold-light) !important; transform: translateY(-1px); }
|
||||
.nav-cta::after { display: none !important; }
|
||||
.hamburger { display: none; flex-direction: column; gap: 5px; cursor: pointer; padding: 8px; border-radius: 8px; transition: background 0.2s; }
|
||||
.hamburger:hover { background: var(--gold-pale); }
|
||||
.hamburger span { display: block; width: 24px; height: 2px; background: var(--dark); border-radius: 2px; transition: transform 0.3s, opacity 0.3s; }
|
||||
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.hamburger.open span:nth-child(2) { opacity: 0; transform: scaleX(0); }
|
||||
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
/* Mobile nav drawer */
|
||||
.mobile-nav {
|
||||
display: none; position: fixed; top: 70px; right: 0; left: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0); z-index: 98; transition: background 0.25s;
|
||||
}
|
||||
.mobile-nav.open { display: block; background: rgba(0,0,0,0.35); }
|
||||
.mobile-nav-drawer {
|
||||
position: absolute; top: 0; right: 0; left: 0;
|
||||
background: var(--white); padding: 1.2rem 2rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
display: flex; flex-direction: column; gap: 0;
|
||||
transform: translateY(-8px); opacity: 0;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
}
|
||||
.mobile-nav.open .mobile-nav-drawer { transform: translateY(0); opacity: 1; }
|
||||
.mobile-nav-drawer a {
|
||||
display: block; padding: 0.85rem 0; color: var(--dark); font-size: 0.95rem;
|
||||
font-weight: 500; border-bottom: 1px solid var(--border); text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.mobile-nav-drawer a:last-child { border-bottom: none; padding-bottom: 0.5rem; }
|
||||
.mobile-nav-drawer a:hover { color: var(--gold); }
|
||||
.mobile-nav-drawer .nav-cta-m {
|
||||
margin-top: 1rem; background: var(--gold); color: var(--white) !important;
|
||||
text-align: center; padding: 0.75rem 1.5rem; border-radius: 50px;
|
||||
font-weight: 600; border-bottom: none !important;
|
||||
}
|
||||
/* Footer */
|
||||
footer { background: var(--dark); color: rgba(255,255,255,0.6); text-align: center; padding: 2.5rem 2rem; font-size: 0.85rem; }
|
||||
footer a { color: var(--gold-light); text-decoration: none; }
|
||||
/* Shared buttons */
|
||||
.btn-primary {
|
||||
background: var(--gold); color: var(--white); border: none; padding: 0.8rem 2rem;
|
||||
border-radius: 50px; font-family: 'Vazirmatn', sans-serif; font-size: 0.95rem; font-weight: 500;
|
||||
cursor: pointer; text-decoration: none; display: inline-block;
|
||||
transition: background 0.25s, transform 0.2s, box-shadow 0.25s;
|
||||
box-shadow: 0 4px 18px rgba(184,149,90,0.25);
|
||||
}
|
||||
.btn-primary:hover { background: var(--gold-light); transform: translateY(-2px); box-shadow: 0 8px 24px rgba(184,149,90,0.35); }
|
||||
.btn-secondary {
|
||||
background: transparent; color: var(--gold); border: 1.5px solid var(--gold);
|
||||
padding: 0.78rem 1.8rem; border-radius: 50px; font-family: 'Vazirmatn', sans-serif;
|
||||
font-size: 0.95rem; font-weight: 500; cursor: pointer; text-decoration: none;
|
||||
display: inline-block; transition: all 0.25s;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--gold); color: var(--white); transform: translateY(-2px); }
|
||||
/* Shared */
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 2rem; }
|
||||
section { padding: 6rem 0; }
|
||||
.section-label { font-size: 0.78rem; font-weight: 600; letter-spacing: 0.1em; color: var(--gold); text-transform: uppercase; margin-bottom: 0.8rem; display: block; }
|
||||
.section-title { font-size: clamp(1.6rem, 3vw, 2.3rem); font-weight: 700; color: var(--dark); margin-bottom: 1.2rem; line-height: 1.35; }
|
||||
.section-desc { font-size: 1rem; color: var(--mid); max-width: 560px; line-height: 1.9; }
|
||||
.divider { width: 60px; height: 3px; background: linear-gradient(90deg, var(--gold), var(--gold-pale)); border-radius: 2px; margin: 1.2rem 0 2.5rem; }
|
||||
/* Animations */
|
||||
@@keyframes fadeUp { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||
.fade-in { opacity: 0; transform: translateY(24px); transition: opacity 0.7s ease, transform 0.7s ease; }
|
||||
.fade-in.visible { opacity: 1; transform: translateY(0); }
|
||||
/* Mobile nav */
|
||||
@@media (max-width: 900px) {
|
||||
nav { display: none; }
|
||||
.hamburger { display: flex; }
|
||||
}
|
||||
/* Small screen tweaks */
|
||||
@@media (max-width: 480px) {
|
||||
header { padding: 0 1rem; }
|
||||
.logo { font-size: 0.88rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a class="logo" href="/">@(ViewData["SiteName"] ?? "دکتر سوسن آلطه")</a>
|
||||
<nav>
|
||||
<a href="/#about">درباره من</a>
|
||||
<a href="/#services">خدمات</a>
|
||||
<a href="/#gallery">گالری</a>
|
||||
<a href="/blog">وبلاگ</a>
|
||||
<a href="/#testimonials">نظرات</a>
|
||||
<a href="/#contact" class="nav-cta">رزرو نوبت</a>
|
||||
</nav>
|
||||
<div class="hamburger" id="hamburger" onclick="toggleMenu()" aria-label="منو" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Mobile nav drawer (separate from header so it can overlay body) -->
|
||||
<div class="mobile-nav" id="mobileNav" onclick="closeMobileNavOnOverlay(event)">
|
||||
<div class="mobile-nav-drawer">
|
||||
<a href="/#about" onclick="closeMenu()">درباره من</a>
|
||||
<a href="/#services" onclick="closeMenu()">خدمات</a>
|
||||
<a href="/#gallery" onclick="closeMenu()">گالری</a>
|
||||
<a href="/blog" onclick="closeMenu()">وبلاگ</a>
|
||||
<a href="/#testimonials" onclick="closeMenu()">نظرات</a>
|
||||
<a href="/#contact" onclick="closeMenu()" class="nav-cta-m">رزرو نوبت</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<footer>
|
||||
<p>© @DateTime.Now.Year @(ViewData["SiteName"] ?? "دکتر سوسن آلطه") — تمامی حقوق محفوظ است.</p>
|
||||
<p style="margin-top:0.5rem; font-size:0.78rem; opacity:0.5;">طراحی شده با ❤ برای سلامت و زیبایی شما</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const _ham = document.getElementById('hamburger');
|
||||
const _nav = document.getElementById('mobileNav');
|
||||
|
||||
function toggleMenu() {
|
||||
const open = _nav.classList.toggle('open');
|
||||
_ham.classList.toggle('open', open);
|
||||
_ham.setAttribute('aria-expanded', String(open));
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
}
|
||||
function closeMenu() {
|
||||
_nav.classList.remove('open');
|
||||
_ham.classList.remove('open');
|
||||
_ham.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
function closeMobileNavOnOverlay(e) {
|
||||
// close only when clicking the dim overlay, not the drawer itself
|
||||
if (e.target === _nav) closeMenu();
|
||||
}
|
||||
// close on Escape key
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
|
||||
// close when viewport widens past mobile breakpoint
|
||||
window.addEventListener('resize', () => { if (window.innerWidth > 900) closeMenu(); });
|
||||
|
||||
// Scroll fade-in
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
||||
}, { threshold: 0.1 });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
|
||||
});
|
||||
</script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
@using DrSousan.Api.Models
|
||||
@using DrSousan.Api.Data
|
||||
@namespace DrSousan.Api.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:18342",
|
||||
"sslPort": 44378
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"applicationUrl": "https://localhost:7001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Data Source=drsousan.db"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "DrSousanSecretKey2024!ChangeThisInProduction!MinLength32Chars",
|
||||
"Issuer": "DrSousanApi",
|
||||
"Audience": "DrSousanAdmin"
|
||||
},
|
||||
"Admin": {
|
||||
"Username": "admin",
|
||||
"Password": "admin123"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
/* site.css — minimal shared overrides only; all page CSS is inline in each Razor page */
|
||||
/* The _Layout.cshtml already defines all base/reset/nav/footer styles inline */
|
||||
@@ -0,0 +1,915 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>پنل مدیریت | دکتر سوسن آلطه</title>
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--gold:#B8955A;--gold-l:#D4B483;--gold-pale:#F5ECD8;
|
||||
--bg:#F4F6F8;--white:#fff;--dark:#1E1E1E;--mid:#5A5A5A;
|
||||
--light:#9A9A9A;--border:#E2E8F0;--danger:#E53935;--success:#2E7D32;
|
||||
--sidebar:240px;
|
||||
}
|
||||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--dark);direction:rtl;display:flex;min-height:100vh}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar{width:var(--sidebar);background:var(--dark);color:#fff;display:flex;flex-direction:column;flex-shrink:0;position:fixed;top:0;right:0;bottom:0;z-index:50}
|
||||
.sidebar-logo{padding:1.5rem 1.2rem;border-bottom:1px solid rgba(255,255,255,.08);font-size:.9rem;font-weight:600;color:var(--gold-l);line-height:1.4}
|
||||
.sidebar-logo span{display:block;font-size:.7rem;color:rgba(255,255,255,.4);font-weight:400;margin-top:.2rem}
|
||||
nav.sidebar-nav{flex:1;overflow-y:auto;padding:.8rem 0}
|
||||
.nav-section{font-size:.65rem;font-weight:600;letter-spacing:.08em;color:rgba(255,255,255,.3);padding:.8rem 1.2rem .3rem;text-transform:uppercase}
|
||||
.nav-item{display:flex;align-items:center;gap:.7rem;padding:.65rem 1.2rem;color:rgba(255,255,255,.65);cursor:pointer;font-size:.85rem;transition:background .2s,color .2s;border-right:3px solid transparent}
|
||||
.nav-item:hover{background:rgba(255,255,255,.05);color:#fff}
|
||||
.nav-item.active{background:rgba(184,149,90,.12);color:var(--gold-l);border-right-color:var(--gold)}
|
||||
.nav-item svg{width:16px;height:16px;flex-shrink:0;opacity:.7}
|
||||
.nav-item.active svg{opacity:1}
|
||||
.sidebar-footer{padding:1rem 1.2rem;border-top:1px solid rgba(255,255,255,.08);font-size:.75rem;color:rgba(255,255,255,.35)}
|
||||
|
||||
/* ── Main ── */
|
||||
.main{margin-right:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
|
||||
.topbar{background:var(--white);border-bottom:1px solid var(--border);padding:.75rem 2rem;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:40}
|
||||
.topbar-title{font-size:1rem;font-weight:600;color:var(--dark)}
|
||||
.topbar-actions{display:flex;gap:.7rem;align-items:center}
|
||||
.page-content{padding:2rem;flex:1}
|
||||
|
||||
/* ── Cards ── */
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.2rem;margin-bottom:2rem}
|
||||
.stat-card{background:var(--white);border-radius:14px;padding:1.4rem 1.6rem;border:1px solid var(--border);display:flex;flex-direction:column;gap:.4rem}
|
||||
.stat-card .label{font-size:.78rem;color:var(--light)}
|
||||
.stat-card .value{font-size:2rem;font-weight:700;color:var(--dark)}
|
||||
.stat-card .icon{width:38px;height:38px;border-radius:10px;background:var(--gold-pale);display:flex;align-items:center;justify-content:center;margin-bottom:.4rem}
|
||||
.stat-card .icon svg{width:18px;height:18px;color:var(--gold)}
|
||||
|
||||
.card{background:var(--white);border-radius:14px;border:1px solid var(--border);overflow:hidden;margin-bottom:1.5rem}
|
||||
.card-header{padding:1.2rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.card-title{font-size:.95rem;font-weight:600}
|
||||
.card-body{padding:1.5rem}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.1rem;border-radius:8px;font-family:inherit;font-size:.85rem;font-weight:500;cursor:pointer;border:1.5px solid transparent;transition:all .2s}
|
||||
.btn-primary{background:var(--gold);color:#fff;border-color:var(--gold)}
|
||||
.btn-primary:hover{background:var(--gold-l)}
|
||||
.btn-secondary{background:transparent;color:var(--mid);border-color:var(--border)}
|
||||
.btn-secondary:hover{border-color:var(--gold);color:var(--gold)}
|
||||
.btn-danger{background:transparent;color:var(--danger);border-color:var(--danger)}
|
||||
.btn-danger:hover{background:var(--danger);color:#fff}
|
||||
.btn-sm{padding:.35rem .7rem;font-size:.78rem}
|
||||
.btn svg{width:15px;height:15px}
|
||||
|
||||
/* ── Table ── */
|
||||
.table-wrap{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:.85rem}
|
||||
th{background:#F8FAFC;padding:.75rem 1rem;text-align:right;font-weight:600;color:var(--mid);font-size:.78rem;border-bottom:1px solid var(--border)}
|
||||
td{padding:.75rem 1rem;border-bottom:1px solid var(--border);color:var(--dark);vertical-align:middle}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#FAFBFC}
|
||||
.badge{display:inline-flex;align-items:center;padding:.2rem .65rem;border-radius:50px;font-size:.72rem;font-weight:600}
|
||||
.badge-green{background:#E8F5E9;color:#2E7D32}
|
||||
.badge-red{background:#FFEBEE;color:#C62828}
|
||||
.badge-gold{background:var(--gold-pale);color:var(--gold)}
|
||||
|
||||
/* ── Forms ── */
|
||||
.form-grid{display:grid;gap:1rem}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.form-group{display:flex;flex-direction:column;gap:.35rem}
|
||||
.form-group label{font-size:.82rem;font-weight:500;color:var(--dark)}
|
||||
.form-group input,.form-group select,.form-group textarea{
|
||||
border:1.5px solid var(--border);border-radius:8px;padding:.65rem .9rem;
|
||||
font-family:inherit;font-size:.88rem;color:var(--dark);direction:rtl;
|
||||
transition:border-color .2s,box-shadow .2s;outline:none;background:var(--white)
|
||||
}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(184,149,90,.1)}
|
||||
.form-group textarea{resize:vertical;min-height:100px}
|
||||
.form-hint{font-size:.72rem;color:var(--light);margin-top:.2rem}
|
||||
.seo-score{display:flex;align-items:center;gap:.5rem;padding:.6rem 1rem;border-radius:8px;font-size:.8rem}
|
||||
.seo-score.good{background:#E8F5E9;color:#2E7D32}
|
||||
.seo-score.ok{background:#FFF3E0;color:#E65100}
|
||||
.seo-score.bad{background:#FFEBEE;color:#C62828}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;display:flex;align-items:center;justify-content:center;padding:1rem}
|
||||
.modal-overlay.hidden{display:none}
|
||||
.modal{background:var(--white);border-radius:16px;width:100%;max-width:720px;max-height:90vh;overflow-y:auto}
|
||||
.modal-header{padding:1.2rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--white);z-index:1}
|
||||
.modal-title{font-size:1rem;font-weight:600}
|
||||
.modal-close{background:none;border:none;cursor:pointer;padding:.4rem;color:var(--light)}
|
||||
.modal-body{padding:1.5rem}
|
||||
.modal-footer{padding:1rem 1.5rem;border-top:1px solid var(--border);display:flex;gap:.7rem;justify-content:flex-start}
|
||||
|
||||
/* ── Toast ── */
|
||||
.toast-container{position:fixed;bottom:1.5rem;left:1.5rem;z-index:300;display:flex;flex-direction:column;gap:.5rem}
|
||||
.toast{background:var(--dark);color:#fff;padding:.7rem 1.2rem;border-radius:10px;font-size:.85rem;animation:slideIn .3s ease;box-shadow:0 4px 20px rgba(0,0,0,.2)}
|
||||
.toast.success{border-left:4px solid #4CAF50}
|
||||
.toast.error{border-left:4px solid var(--danger)}
|
||||
@keyframes slideIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* ── Rich text editor minimal ── */
|
||||
.editor-toolbar{border:1.5px solid var(--border);border-bottom:none;border-radius:8px 8px 0 0;padding:.4rem .6rem;display:flex;gap:.3rem;flex-wrap:wrap;background:#F8FAFC}
|
||||
.editor-toolbar button{background:none;border:none;padding:.3rem .5rem;border-radius:4px;cursor:pointer;font-size:.82rem;font-family:inherit;color:var(--mid)}
|
||||
.editor-toolbar button:hover{background:var(--gold-pale);color:var(--gold)}
|
||||
.editor-content{border:1.5px solid var(--border);border-radius:0 0 8px 8px;padding:.8rem 1rem;min-height:250px;outline:none;font-size:.9rem;line-height:1.8;direction:rtl}
|
||||
|
||||
/* ── Pages ── */
|
||||
.page{display:none}
|
||||
.page.active{display:block}
|
||||
|
||||
/* ── Login ── */
|
||||
.login-screen{position:fixed;inset:0;background:var(--dark);display:flex;align-items:center;justify-content:center;z-index:1000}
|
||||
.login-box{background:var(--white);border-radius:20px;padding:2.5rem;width:380px;text-align:center}
|
||||
.login-logo{font-size:1.1rem;font-weight:700;color:var(--gold);margin-bottom:.3rem}
|
||||
.login-sub{font-size:.8rem;color:var(--light);margin-bottom:2rem}
|
||||
.login-screen.hidden{display:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ══ LOGIN ══ -->
|
||||
<div class="login-screen" id="loginScreen">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">دکتر سوسن آلطه</div>
|
||||
<div class="login-sub">پنل مدیریت محتوا</div>
|
||||
<div class="form-group" style="margin-bottom:1rem">
|
||||
<label>نام کاربری</label>
|
||||
<input type="text" id="loginUser" value="admin" placeholder="نام کاربری"/>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:1.5rem">
|
||||
<label>رمز عبور</label>
|
||||
<input type="password" id="loginPass" value="admin123" placeholder="رمز عبور"/>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width:100%;justify-content:center" onclick="doLogin()">ورود به پنل</button>
|
||||
<p id="loginErr" style="color:var(--danger);font-size:.8rem;margin-top:.8rem;display:none">نام کاربری یا رمز عبور اشتباه است</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SIDEBAR ══ -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-logo">مدیریت سایت<span>دکتر سوسن آلطه</span></div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">داشبورد</div>
|
||||
<div class="nav-item active" onclick="showPage('dashboard',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
داشبورد
|
||||
</div>
|
||||
<div class="nav-section">محتوای سایت</div>
|
||||
<div class="nav-item" onclick="showPage('hero',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
||||
صفحه اصلی (هرو)
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('about',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>
|
||||
درباره من
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('contact',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
اطلاعات تماس
|
||||
</div>
|
||||
<div class="nav-section">بخشهای سایت</div>
|
||||
<div class="nav-item" onclick="showPage('services',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||||
خدمات
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('gallery',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
گالری
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('testimonials',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
نظرات بیماران
|
||||
</div>
|
||||
<div class="nav-section">وبلاگ و SEO</div>
|
||||
<div class="nav-item" onclick="showPage('blogposts',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
مقالات
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('categories',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
دستهبندیها
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('seo',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
گزارش SEO
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">API: <span id="apiLabel">localhost:5000</span></div>
|
||||
</aside>
|
||||
|
||||
<!-- ══ MAIN ══ -->
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" id="pageTitle">داشبورد</div>
|
||||
<div class="topbar-actions">
|
||||
<a href="../index.html" target="_blank" class="btn btn-secondary btn-sm">مشاهده سایت</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="logout()">خروج</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ── DASHBOARD ── -->
|
||||
<div class="page active" id="page-dashboard">
|
||||
<div class="stat-grid" id="dashStats">
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div><div class="label">مقالات منتشرشده</div><div class="value" id="ds-posts">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></div><div class="label">کل بازدید مقالات</div><div class="value" id="ds-views">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div><div class="label">نظرات بیماران</div><div class="value" id="ds-testimonials">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></div><div class="label">مقالات بدون متا</div><div class="value" id="ds-nometa" style="color:var(--danger)">-</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">پربازدیدترین مقالات</div></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>عنوان</th><th>بازدید</th><th>عملیات</th></tr></thead><tbody id="topPostsTable"></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── HERO ── -->
|
||||
<div class="page" id="page-hero">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">ویرایش بخش هرو</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نام دکتر</label><input id="hero-name"/></div>
|
||||
<div class="form-group"><label>تخصص (زیر نام)</label><input id="hero-title"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>توضیح کوتاه</label><textarea id="hero-subtitle" rows="2"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۱ - عدد</label><input id="hero-stat1_num"/></div>
|
||||
<div class="form-group"><label>آمار ۱ - برچسب</label><input id="hero-stat1_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۲ - عدد</label><input id="hero-stat2_num"/></div>
|
||||
<div class="form-group"><label>آمار ۲ - برچسب</label><input id="hero-stat2_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۳ - عدد</label><input id="hero-stat3_num"/></div>
|
||||
<div class="form-group"><label>آمار ۳ - برچسب</label><input id="hero-stat3_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>دکمه اصلی</label><input id="hero-cta_primary"/></div>
|
||||
<div class="form-group"><label>دکمه ثانویه</label><input id="hero-cta_secondary"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('hero')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── ABOUT ── -->
|
||||
<div class="page" id="page-about">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">ویرایش بخش درباره من</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>عنوان</label><input id="about-title"/></div>
|
||||
<div class="form-group"><label>سالهای تجربه</label><input id="about-years_exp"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>بیوگرافی</label><textarea id="about-bio" rows="4"></textarea></div>
|
||||
<div class="form-group"><label>دستاورد ۱</label><input id="about-cred1"/></div>
|
||||
<div class="form-group"><label>دستاورد ۲</label><input id="about-cred2"/></div>
|
||||
<div class="form-group"><label>دستاورد ۳</label><input id="about-cred3"/></div>
|
||||
<div class="form-group"><label>دستاورد ۴</label><input id="about-cred4"/></div>
|
||||
<div class="form-group"><label>دستاورد ۵</label><input id="about-cred5"/></div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('about')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CONTACT ── -->
|
||||
<div class="page" id="page-contact">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">اطلاعات تماس</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>تلفن</label><input id="contact-phone"/></div>
|
||||
<div class="form-group"><label>ایمیل</label><input id="contact-email"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>آدرس</label><input id="contact-address"/></div>
|
||||
<div class="form-group"><label>ساعات کاری</label><input id="contact-hours"/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>لینک اینستاگرام</label><input id="contact-instagram" dir="ltr"/></div>
|
||||
<div class="form-group"><label>لینک واتساپ</label><input id="contact-whatsapp" dir="ltr"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>لینک تلگرام</label><input id="contact-telegram" dir="ltr"/></div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('contact')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SERVICES ── -->
|
||||
<div class="page" id="page-services">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">خدمات</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openSvcModal()">+ افزودن خدمت</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>#</th><th>عنوان</th><th>توضیح</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="servicesTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── GALLERY ── -->
|
||||
<div class="page" id="page-gallery">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">گالری</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openGalleryModal()">+ افزودن تصویر</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>دسته</th><th>توضیح</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="galleryTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TESTIMONIALS ── -->
|
||||
<div class="page" id="page-testimonials">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">نظرات بیماران</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openTestimModal()">+ افزودن نظر</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>نام</th><th>متن</th><th>امتیاز</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="testimonialsTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── BLOG POSTS ── -->
|
||||
<div class="page" id="page-blogposts">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">مقالات وبلاگ</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openPostEditor()">+ مقاله جدید</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>عنوان</th><th>دسته</th><th>کلیدواژه</th><th>بازدید</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="postsTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CATEGORIES ── -->
|
||||
<div class="page" id="page-categories">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">دستهبندیها</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openCatModal()">+ دسته جدید</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>نام</th><th>اسلاگ</th><th>مقالات</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="categoriesTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SEO ── -->
|
||||
<div class="page" id="page-seo">
|
||||
<div class="stat-grid" style="grid-template-columns:repeat(3,1fr)">
|
||||
<div class="stat-card"><div class="label">مقالات منتشرشده</div><div class="value" id="seo-posts">-</div></div>
|
||||
<div class="stat-card"><div class="label">کل بازدید</div><div class="value" id="seo-views">-</div></div>
|
||||
<div class="stat-card"><div class="label">مقالات بدون متا</div><div class="value" id="seo-nometa" style="color:var(--danger)">-</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">پربازدیدترین مقالات</div></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>عنوان</th><th>بازدید</th></tr></thead><tbody id="seoTopPosts"></tbody></table></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">راهنمای بهبود SEO</div></div>
|
||||
<div class="card-body" style="font-size:.88rem;line-height:2;color:var(--mid)">
|
||||
<p>✅ هر مقاله باید <strong>MetaTitle</strong> زیر ۷۰ کاراکتر داشته باشد</p>
|
||||
<p>✅ <strong>MetaDescription</strong> بین ۱۵۰-۱۶۰ کاراکتر</p>
|
||||
<p>✅ <strong>کلیدواژه اصلی</strong> در عنوان، اول پاراگراف و URL وجود داشته باشد</p>
|
||||
<p>✅ هر مقاله حداقل <strong>۵۰۰ کلمه</strong> داشته باشد</p>
|
||||
<p>✅ از <strong>H2 و H3</strong> در ساختار مقاله استفاده کنید</p>
|
||||
<p>✅ لینکدهی داخلی بین مقالات انجام دهید</p>
|
||||
<p>✅ تصاویر دارای <strong>alt text</strong> فارسی باشند</p>
|
||||
<p>🔗 <a href="../sitemap.xml" target="_blank" style="color:var(--gold)">مشاهده Sitemap</a> | <a href="../robots.txt" target="_blank" style="color:var(--gold)">مشاهده Robots.txt</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /page-content -->
|
||||
</div><!-- /main -->
|
||||
|
||||
<!-- ══ MODALS ══ -->
|
||||
|
||||
<!-- Service Modal -->
|
||||
<div class="modal-overlay hidden" id="svcModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title" id="svcModalTitle">افزودن خدمت</div><button class="modal-close" onclick="closeModal('svcModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="svc-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>عنوان</label><input id="svc-title"/></div>
|
||||
<div class="form-group"><label>توضیح</label><textarea id="svc-desc" rows="3"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>ترتیب</label><input type="number" id="svc-order" value="1"/></div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="svc-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveSvc()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('svcModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Modal -->
|
||||
<div class="modal-overlay hidden" id="galleryModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title" id="galleryModalTitle">افزودن تصویر</div><button class="modal-close" onclick="closeModal('galleryModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="gal-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>آدرس تصویر اصلی</label><input id="gal-img" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آدرس تصویر قبل</label><input id="gal-before" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-group"><label>آدرس تصویر بعد</label><input id="gal-after" dir="ltr" placeholder="https://..."/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>دستهبندی</label><input id="gal-cat" placeholder="بوتاکس، لیزر..."/></div>
|
||||
<div class="form-group"><label>ترتیب</label><input type="number" id="gal-order" value="1"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>توضیح</label><input id="gal-caption"/></div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="gal-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveGallery()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('galleryModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonial Modal -->
|
||||
<div class="modal-overlay hidden" id="testimModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title">افزودن / ویرایش نظر</div><button class="modal-close" onclick="closeModal('testimModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="testim-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نام</label><input id="testim-name"/></div>
|
||||
<div class="form-group"><label>ایموجی</label><input id="testim-emoji" value="👩"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>متن نظر</label><textarea id="testim-text" rows="3"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>امتیاز (۱-۵)</label><input type="number" id="testim-rating" value="5" min="1" max="5"/></div>
|
||||
<div class="form-group"><label>تاریخ</label><input id="testim-date" placeholder="اردیبهشت ۱۴۰۳"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="testim-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveTestim()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('testimModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Modal -->
|
||||
<div class="modal-overlay hidden" id="catModal">
|
||||
<div class="modal" style="max-width:480px">
|
||||
<div class="modal-header"><div class="modal-title" id="catModalTitle">دسته جدید</div><button class="modal-close" onclick="closeModal('catModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="cat-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>نام</label><input id="cat-name"/></div>
|
||||
<div class="form-group"><label>اسلاگ (خودکار)</label><input id="cat-slug" dir="ltr" placeholder="auto-generated"/></div>
|
||||
<div class="form-group"><label>توضیح</label><textarea id="cat-desc" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveCat()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('catModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog Post Editor Modal -->
|
||||
<div class="modal-overlay hidden" id="postModal">
|
||||
<div class="modal" style="max-width:900px">
|
||||
<div class="modal-header"><div class="modal-title" id="postModalTitle">مقاله جدید</div><button class="modal-close" onclick="closeModal('postModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="post-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>عنوان مقاله</label><input id="post-title" oninput="autoSlug()"/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>اسلاگ (URL)</label><input id="post-slug" dir="ltr"/></div>
|
||||
<div class="form-group"><label>دستهبندی</label><select id="post-category"><option value="">انتخاب دسته...</option></select></div>
|
||||
</div>
|
||||
<div class="form-group"><label>خلاصه (Excerpt)</label><textarea id="post-excerpt" rows="2"></textarea></div>
|
||||
<div class="form-group">
|
||||
<label>محتوای مقاله</label>
|
||||
<div class="editor-toolbar">
|
||||
<button onclick="fmt('bold')"><b>B</b></button>
|
||||
<button onclick="fmt('italic')"><i>I</i></button>
|
||||
<button onclick="fmtBlock('h2')">H2</button>
|
||||
<button onclick="fmtBlock('h3')">H3</button>
|
||||
<button onclick="fmtBlock('p')">P</button>
|
||||
<button onclick="fmt('insertUnorderedList')">• لیست</button>
|
||||
<button onclick="fmt('insertOrderedList')">۱. لیست</button>
|
||||
<button onclick="insLink()">🔗 لینک</button>
|
||||
</div>
|
||||
<div class="editor-content" id="post-content" contenteditable="true" dir="rtl"></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:1rem;margin-top:.5rem">
|
||||
<p style="font-size:.82rem;font-weight:600;color:var(--gold);margin-bottom:1rem">تنظیمات SEO</p>
|
||||
<div class="form-group"><label>کلیدواژه اصلی (Focus Keyword)</label><input id="post-focus" oninput="checkSeo()"/><div class="form-hint">کلیدواژهای که میخواهید برای آن رتبه بگیرید</div></div>
|
||||
<div id="seoFeedback" style="margin:.5rem 0"></div>
|
||||
<div class="form-group"><label>Meta Title <span style="font-weight:400;color:var(--light)" id="mtLen">(0/70)</span></label><input id="post-metatitle" oninput="updateLen('post-metatitle','mtLen',70)"/></div>
|
||||
<div class="form-group"><label>Meta Description <span style="font-weight:400;color:var(--light)" id="mdLen">(0/160)</span></label><textarea id="post-metadesc" rows="2" oninput="updateLen('post-metadesc','mdLen',160)"></textarea></div>
|
||||
<div class="form-group"><label>کلیدواژهها (با کاما جدا کنید)</label><input id="post-keywords" placeholder="بوتاکس, جوانسازی, پوست..."/></div>
|
||||
<div class="form-group"><label>تصویر شاخص</label><input id="post-image" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نوع Schema</label><select id="post-schematype"><option value="MedicalWebPage">MedicalWebPage</option><option value="Article">Article</option><option value="FAQPage">FAQPage</option></select></div>
|
||||
<div class="form-group"><label>وضعیت انتشار</label><select id="post-published"><option value="false">پیشنویس</option><option value="true">منتشر</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="savePost()">ذخیره مقاله</button><button class="btn btn-secondary" onclick="closeModal('postModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script>
|
||||
const API = 'http://localhost:5000';
|
||||
let token = localStorage.getItem('dr_token') || '';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
async function doLogin() {
|
||||
const u = document.getElementById('loginUser').value;
|
||||
const p = document.getElementById('loginPass').value;
|
||||
const r = await fetch(`${API}/api/auth/login`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({username:u, password:p})
|
||||
});
|
||||
if (!r.ok) { document.getElementById('loginErr').style.display='block'; return; }
|
||||
const d = await r.json();
|
||||
token = d.token;
|
||||
localStorage.setItem('dr_token', token);
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
init();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('dr_token');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// ── API helper ────────────────────────────────────────────────────────────────
|
||||
async function api(path, opts={}) {
|
||||
const h = {'Content-Type':'application/json'};
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
const r = await fetch(`${API}${path}`, {...opts, headers:{...h, ...(opts.headers||{})}});
|
||||
if (r.status === 401) { logout(); return null; }
|
||||
if (r.status === 204) return null;
|
||||
if (!r.ok) { toast('خطا در سرور: ' + r.status, 'error'); return null; }
|
||||
return r.json().catch(()=>null);
|
||||
}
|
||||
|
||||
function toast(msg, type='success') {
|
||||
const c = document.getElementById('toastContainer');
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast ${type}`;
|
||||
el.textContent = msg;
|
||||
c.appendChild(el);
|
||||
setTimeout(()=>el.remove(), 3500);
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دستهبندیها',seo:'گزارش SEO'};
|
||||
|
||||
function showPage(name, el) {
|
||||
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
|
||||
document.getElementById('page-'+name).classList.add('active');
|
||||
el.classList.add('active');
|
||||
document.getElementById('pageTitle').textContent = pageTitles[name] || name;
|
||||
loadPage(name);
|
||||
}
|
||||
|
||||
function loadPage(name) {
|
||||
if (name==='dashboard') loadDashboard();
|
||||
else if (name==='hero') loadSection('hero');
|
||||
else if (name==='about') loadSection('about');
|
||||
else if (name==='contact') loadSection('contact');
|
||||
else if (name==='services') loadServices();
|
||||
else if (name==='gallery') loadGallery();
|
||||
else if (name==='testimonials') loadTestimonials();
|
||||
else if (name==='blogposts') loadPosts();
|
||||
else if (name==='categories') loadCategories();
|
||||
else if (name==='seo') loadSeo();
|
||||
}
|
||||
|
||||
// ── Dashboard ─────────────────────────────────────────────────────────────────
|
||||
async function loadDashboard() {
|
||||
const [seo, testims] = await Promise.all([
|
||||
api('/api/seo/stats'),
|
||||
api('/api/testimonials/all')
|
||||
]);
|
||||
if (seo) {
|
||||
document.getElementById('ds-posts').textContent = seo.total;
|
||||
document.getElementById('ds-views').textContent = seo.views;
|
||||
document.getElementById('ds-nometa').textContent = seo.noMeta;
|
||||
const tb = document.getElementById('topPostsTable');
|
||||
tb.innerHTML = seo.topPosts.map(p=>`<tr><td>${p.title}</td><td><span class="badge badge-gold">${p.viewCount}</span></td><td><a href="../post.html?slug=${p.slug}" target="_blank" class="btn btn-secondary btn-sm">مشاهده</a></td></tr>`).join('');
|
||||
}
|
||||
if (testims) document.getElementById('ds-testimonials').textContent = testims.length;
|
||||
}
|
||||
|
||||
// ── Section settings ──────────────────────────────────────────────────────────
|
||||
async function loadSection(sec) {
|
||||
const data = await api(`/api/settings/${sec}`);
|
||||
if (!data) return;
|
||||
Object.entries(data).forEach(([k,v])=>{
|
||||
const el = document.getElementById(`${sec}-${k}`);
|
||||
if (el) el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSection(sec) {
|
||||
const inputs = document.querySelectorAll(`[id^="${sec}-"]`);
|
||||
const settings = {};
|
||||
inputs.forEach(el=>{
|
||||
const key = el.id.replace(`${sec}-`,'');
|
||||
settings[key] = el.value;
|
||||
});
|
||||
await api(`/api/settings/${sec}`, {method:'PUT', body:JSON.stringify({settings})});
|
||||
toast('تغییرات با موفقیت ذخیره شد ✓');
|
||||
}
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────────────
|
||||
let svcs=[];
|
||||
async function loadServices() {
|
||||
svcs = await api('/api/services/all') || [];
|
||||
document.getElementById('servicesTable').innerHTML = svcs.map(s=>`
|
||||
<tr>
|
||||
<td>${s.order}</td>
|
||||
<td><strong>${s.title}</strong></td>
|
||||
<td style="max-width:300px;color:var(--mid)">${s.description.substring(0,60)}...</td>
|
||||
<td><span class="badge ${s.isActive?'badge-green':'badge-red'}">${s.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editSvc(${s.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteSvc(${s.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openSvcModal(id) {
|
||||
document.getElementById('svcModalTitle').textContent = 'افزودن خدمت';
|
||||
document.getElementById('svc-id').value='';
|
||||
document.getElementById('svc-title').value='';
|
||||
document.getElementById('svc-desc').value='';
|
||||
document.getElementById('svc-order').value=svcs.length+1;
|
||||
document.getElementById('svc-active').value='true';
|
||||
document.getElementById('svcModal').classList.remove('hidden');
|
||||
}
|
||||
function editSvc(id) {
|
||||
const s = svcs.find(x=>x.id===id);
|
||||
document.getElementById('svcModalTitle').textContent = 'ویرایش خدمت';
|
||||
document.getElementById('svc-id').value=s.id;
|
||||
document.getElementById('svc-title').value=s.title;
|
||||
document.getElementById('svc-desc').value=s.description;
|
||||
document.getElementById('svc-order').value=s.order;
|
||||
document.getElementById('svc-active').value=String(s.isActive);
|
||||
document.getElementById('svcModal').classList.remove('hidden');
|
||||
}
|
||||
async function saveSvc() {
|
||||
const id = document.getElementById('svc-id').value;
|
||||
const body = {
|
||||
title: document.getElementById('svc-title').value,
|
||||
description: document.getElementById('svc-desc').value,
|
||||
order: parseInt(document.getElementById('svc-order').value),
|
||||
isActive: document.getElementById('svc-active').value==='true',
|
||||
iconSvg:''
|
||||
};
|
||||
if (id) await api(`/api/services/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/services',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('svcModal');
|
||||
toast('خدمت ذخیره شد ✓');
|
||||
loadServices();
|
||||
}
|
||||
async function deleteSvc(id) {
|
||||
if (!confirm('حذف شود؟')) return;
|
||||
await api(`/api/services/${id}`,{method:'DELETE'});
|
||||
toast('حذف شد','error');
|
||||
loadServices();
|
||||
}
|
||||
|
||||
// ── Gallery ───────────────────────────────────────────────────────────────────
|
||||
let gals=[];
|
||||
async function loadGallery() {
|
||||
gals = await api('/api/gallery/all') || [];
|
||||
document.getElementById('galleryTable').innerHTML = gals.map(g=>`
|
||||
<tr>
|
||||
<td><span class="badge badge-gold">${g.category||'—'}</span></td>
|
||||
<td>${g.caption||'—'}</td>
|
||||
<td><span class="badge ${g.isActive?'badge-green':'badge-red'}">${g.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editGallery(${g.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteGallery(${g.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openGalleryModal() {
|
||||
['id','img','before','after','cat','caption'].forEach(k=>document.getElementById(`gal-${k}`).value='');
|
||||
document.getElementById('gal-order').value=gals.length+1;
|
||||
document.getElementById('gal-active').value='true';
|
||||
document.getElementById('galleryModal').classList.remove('hidden');
|
||||
}
|
||||
function editGallery(id) {
|
||||
const g=gals.find(x=>x.id===id);
|
||||
document.getElementById('gal-id').value=g.id;
|
||||
document.getElementById('gal-img').value=g.imageUrl;
|
||||
document.getElementById('gal-before').value=g.beforeImageUrl;
|
||||
document.getElementById('gal-after').value=g.afterImageUrl;
|
||||
document.getElementById('gal-cat').value=g.category;
|
||||
document.getElementById('gal-caption').value=g.caption;
|
||||
document.getElementById('gal-order').value=g.order;
|
||||
document.getElementById('gal-active').value=String(g.isActive);
|
||||
document.getElementById('galleryModal').classList.remove('hidden');
|
||||
}
|
||||
async function saveGallery() {
|
||||
const id=document.getElementById('gal-id').value;
|
||||
const body={imageUrl:document.getElementById('gal-img').value,beforeImageUrl:document.getElementById('gal-before').value,afterImageUrl:document.getElementById('gal-after').value,category:document.getElementById('gal-cat').value,caption:document.getElementById('gal-caption').value,order:parseInt(document.getElementById('gal-order').value),isActive:document.getElementById('gal-active').value==='true'};
|
||||
if(id) await api(`/api/gallery/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/gallery',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('galleryModal'); toast('ذخیره شد ✓'); loadGallery();
|
||||
}
|
||||
async function deleteGallery(id){if(!confirm('حذف؟'))return;await api(`/api/gallery/${id}`,{method:'DELETE'});toast('حذف شد','error');loadGallery();}
|
||||
|
||||
// ── Testimonials ──────────────────────────────────────────────────────────────
|
||||
let testims=[];
|
||||
async function loadTestimonials() {
|
||||
testims=await api('/api/testimonials/all')||[];
|
||||
document.getElementById('testimonialsTable').innerHTML=testims.map(t=>`
|
||||
<tr>
|
||||
<td>${t.authorEmoji} ${t.authorName}</td>
|
||||
<td style="max-width:250px">${t.text.substring(0,60)}...</td>
|
||||
<td>${'★'.repeat(t.rating)}</td>
|
||||
<td><span class="badge ${t.isActive?'badge-green':'badge-red'}">${t.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editTestim(${t.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteTestim(${t.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openTestimModal(){['id','name','emoji','text','date'].forEach(k=>document.getElementById(`testim-${k}`).value='');document.getElementById('testim-emoji').value='👩';document.getElementById('testim-rating').value=5;document.getElementById('testim-active').value='true';document.getElementById('testimModal').classList.remove('hidden');}
|
||||
function editTestim(id){const t=testims.find(x=>x.id===id);document.getElementById('testim-id').value=t.id;document.getElementById('testim-name').value=t.authorName;document.getElementById('testim-emoji').value=t.authorEmoji;document.getElementById('testim-text').value=t.text;document.getElementById('testim-rating').value=t.rating;document.getElementById('testim-date').value=t.date;document.getElementById('testim-active').value=String(t.isActive);document.getElementById('testimModal').classList.remove('hidden');}
|
||||
async function saveTestim(){const id=document.getElementById('testim-id').value;const body={authorName:document.getElementById('testim-name').value,authorEmoji:document.getElementById('testim-emoji').value,text:document.getElementById('testim-text').value,rating:parseInt(document.getElementById('testim-rating').value),date:document.getElementById('testim-date').value,isActive:document.getElementById('testim-active').value==='true'};if(id)await api(`/api/testimonials/${id}`,{method:'PUT',body:JSON.stringify(body)});else await api('/api/testimonials',{method:'POST',body:JSON.stringify(body)});closeModal('testimModal');toast('ذخیره شد ✓');loadTestimonials();}
|
||||
async function deleteTestim(id){if(!confirm('حذف؟'))return;await api(`/api/testimonials/${id}`,{method:'DELETE'});toast('حذف شد','error');loadTestimonials();}
|
||||
|
||||
// ── Blog Categories ───────────────────────────────────────────────────────────
|
||||
let cats=[];
|
||||
async function loadCategories(){
|
||||
cats=await api('/api/blog/categories')||[];
|
||||
document.getElementById('categoriesTable').innerHTML=cats.map(c=>`<tr><td><strong>${c.name}</strong></td><td dir="ltr" style="color:var(--light)">${c.slug}</td><td><span class="badge badge-gold">${c.postCount}</span></td><td><button class="btn btn-secondary btn-sm" onclick="editCat(${c.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteCat(${c.id})">حذف</button></td></tr>`).join('');
|
||||
}
|
||||
function openCatModal(){document.getElementById('catModalTitle').textContent='دسته جدید';['id','name','slug','desc'].forEach(k=>document.getElementById(`cat-${k}`).value='');document.getElementById('catModal').classList.remove('hidden');}
|
||||
function editCat(id){const c=cats.find(x=>x.id===id);document.getElementById('catModalTitle').textContent='ویرایش دسته';document.getElementById('cat-id').value=c.id;document.getElementById('cat-name').value=c.name;document.getElementById('cat-slug').value=c.slug;document.getElementById('cat-desc').value=c.description;document.getElementById('catModal').classList.remove('hidden');}
|
||||
async function saveCat(){const id=document.getElementById('cat-id').value;const body={name:document.getElementById('cat-name').value,slug:document.getElementById('cat-slug').value,description:document.getElementById('cat-desc').value};if(id)await api(`/api/blog/categories/${id}`,{method:'PUT',body:JSON.stringify(body)});else await api('/api/blog/categories',{method:'POST',body:JSON.stringify(body)});closeModal('catModal');toast('ذخیره شد ✓');loadCategories();}
|
||||
async function deleteCat(id){if(!confirm('حذف؟'))return;await api(`/api/blog/categories/${id}`,{method:'DELETE'});toast('حذف شد','error');loadCategories();}
|
||||
|
||||
// ── Blog Posts ────────────────────────────────────────────────────────────────
|
||||
let posts=[];
|
||||
async function loadPosts(){
|
||||
posts=await api('/api/blog/posts/admin')||[];
|
||||
document.getElementById('postsTable').innerHTML=posts.map(p=>`
|
||||
<tr>
|
||||
<td><strong>${p.title}</strong></td>
|
||||
<td><span class="badge badge-gold">${p.category?.name||'—'}</span></td>
|
||||
<td style="color:var(--gold);font-size:.8rem">${p.focusKeyword||'—'}</td>
|
||||
<td>${p.viewCount}</td>
|
||||
<td><span class="badge ${p.isPublished?'badge-green':'badge-red'}">${p.isPublished?'منتشر':'پیشنویس'}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-sm" onclick="editPost(${p.id})">ویرایش</button>
|
||||
<a href="../post.html?slug=${p.slug}" target="_blank" class="btn btn-secondary btn-sm">مشاهده</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="deletePost(${p.id})">حذف</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function openPostEditor(){
|
||||
if(!cats.length) await loadCategories();
|
||||
const catSel=document.getElementById('post-category');
|
||||
catSel.innerHTML='<option value="">انتخاب دسته...</option>'+cats.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');
|
||||
document.getElementById('postModalTitle').textContent='مقاله جدید';
|
||||
['id','title','slug','focus','metatitle','metadesc','keywords','image'].forEach(k=>document.getElementById(`post-${k}`).value='');
|
||||
document.getElementById('post-content').innerHTML='';
|
||||
document.getElementById('post-excerpt').value='';
|
||||
document.getElementById('post-published').value='false';
|
||||
document.getElementById('post-schematype').value='MedicalWebPage';
|
||||
document.getElementById('post-category').value='';
|
||||
updateLen('post-metatitle','mtLen',70);
|
||||
updateLen('post-metadesc','mdLen',160);
|
||||
document.getElementById('postModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function editPost(id){
|
||||
await openPostEditor();
|
||||
const p=await api(`/api/blog/posts/id/${id}`);
|
||||
if(!p)return;
|
||||
document.getElementById('postModalTitle').textContent='ویرایش مقاله';
|
||||
document.getElementById('post-id').value=p.id;
|
||||
document.getElementById('post-title').value=p.title;
|
||||
document.getElementById('post-slug').value=p.slug;
|
||||
document.getElementById('post-excerpt').value=p.excerpt;
|
||||
document.getElementById('post-content').innerHTML=p.content;
|
||||
document.getElementById('post-focus').value=p.focusKeyword;
|
||||
document.getElementById('post-metatitle').value=p.metaTitle;
|
||||
document.getElementById('post-metadesc').value=p.metaDescription;
|
||||
document.getElementById('post-keywords').value=p.keywords;
|
||||
document.getElementById('post-image').value=p.featuredImage;
|
||||
document.getElementById('post-published').value=String(p.isPublished);
|
||||
document.getElementById('post-schematype').value=p.articleType;
|
||||
document.getElementById('post-category').value=p.categoryId||'';
|
||||
updateLen('post-metatitle','mtLen',70);
|
||||
updateLen('post-metadesc','mdLen',160);
|
||||
checkSeo();
|
||||
}
|
||||
|
||||
async function savePost(){
|
||||
const id=document.getElementById('post-id').value;
|
||||
const catId=document.getElementById('post-category').value;
|
||||
const body={
|
||||
title:document.getElementById('post-title').value,
|
||||
slug:document.getElementById('post-slug').value,
|
||||
excerpt:document.getElementById('post-excerpt').value,
|
||||
content:document.getElementById('post-content').innerHTML,
|
||||
featuredImage:document.getElementById('post-image').value,
|
||||
author:'دکتر سوسن آلطه',
|
||||
metaTitle:document.getElementById('post-metatitle').value,
|
||||
metaDescription:document.getElementById('post-metadesc').value,
|
||||
focusKeyword:document.getElementById('post-focus').value,
|
||||
keywords:document.getElementById('post-keywords').value,
|
||||
articleType:document.getElementById('post-schematype').value,
|
||||
isPublished:document.getElementById('post-published').value==='true',
|
||||
categoryId:catId?parseInt(catId):null,
|
||||
ogImage:document.getElementById('post-image').value,
|
||||
};
|
||||
if(id) await api(`/api/blog/posts/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/blog/posts',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('postModal'); toast('مقاله ذخیره شد ✓'); loadPosts();
|
||||
}
|
||||
|
||||
async function deletePost(id){if(!confirm('حذف مقاله؟'))return;await api(`/api/blog/posts/${id}`,{method:'DELETE'});toast('مقاله حذف شد','error');loadPosts();}
|
||||
|
||||
// ── SEO ───────────────────────────────────────────────────────────────────────
|
||||
async function loadSeo(){
|
||||
const s=await api('/api/seo/stats');
|
||||
if(!s)return;
|
||||
document.getElementById('seo-posts').textContent=s.total;
|
||||
document.getElementById('seo-views').textContent=s.views;
|
||||
document.getElementById('seo-nometa').textContent=s.noMeta;
|
||||
document.getElementById('seoTopPosts').innerHTML=s.topPosts.map(p=>`<tr><td>${p.title}</td><td><span class="badge badge-gold">${p.viewCount}</span></td></tr>`).join('');
|
||||
}
|
||||
|
||||
// ── SEO helpers ───────────────────────────────────────────────────────────────
|
||||
function autoSlug(){
|
||||
const t=document.getElementById('post-title').value;
|
||||
const s=t.trim().replace(/\s+/g,'-').replace(/[^-ۿa-z0-9\-]/g,'').toLowerCase();
|
||||
document.getElementById('post-slug').value=s;
|
||||
checkSeo();
|
||||
}
|
||||
|
||||
function updateLen(inputId,labelId,max){
|
||||
const el=document.getElementById(inputId);
|
||||
const len=el.value.length;
|
||||
const label=document.getElementById(labelId);
|
||||
label.textContent=`(${len}/${max})`;
|
||||
label.style.color=len>max?'var(--danger)':(len>max*.8?'var(--gold)':'var(--light)');
|
||||
}
|
||||
|
||||
function checkSeo(){
|
||||
const kw=document.getElementById('post-focus').value.trim();
|
||||
const title=document.getElementById('post-title').value;
|
||||
const mt=document.getElementById('post-metatitle').value;
|
||||
const md=document.getElementById('post-metadesc').value;
|
||||
const fb=document.getElementById('seoFeedback');
|
||||
if(!kw){fb.innerHTML='';return;}
|
||||
const checks=[
|
||||
{ok:title.includes(kw), msg:'کلیدواژه در عنوان'},
|
||||
{ok:mt.includes(kw), msg:'کلیدواژه در Meta Title'},
|
||||
{ok:md.includes(kw), msg:'کلیدواژه در Meta Description'},
|
||||
{ok:mt.length<=70&&mt.length>0, msg:'طول Meta Title مناسب'},
|
||||
{ok:md.length<=160&&md.length>100, msg:'طول Meta Description مناسب'},
|
||||
];
|
||||
const pass=checks.filter(c=>c.ok).length;
|
||||
const cls=pass>=4?'good':pass>=2?'ok':'bad';
|
||||
const emoji=pass>=4?'✅':pass>=2?'⚠️':'❌';
|
||||
fb.innerHTML=`<div class="seo-score ${cls}">${emoji} امتیاز SEO: ${pass}/${checks.length} — ${checks.map(c=>`<span style="opacity:${c.ok?1:.4}">${c.ok?'✓':'✗'} ${c.msg}</span>`).join(' | ')}</div>`;
|
||||
}
|
||||
|
||||
// ── Editor helpers ────────────────────────────────────────────────────────────
|
||||
function fmt(cmd){document.getElementById('post-content').focus();document.execCommand(cmd);}
|
||||
function fmtBlock(tag){document.getElementById('post-content').focus();document.execCommand('formatBlock',false,tag);}
|
||||
function insLink(){const url=prompt('آدرس لینک:');if(url){document.getElementById('post-content').focus();document.execCommand('createLink',false,url);}}
|
||||
|
||||
// ── Modal helpers ─────────────────────────────────────────────────────────────
|
||||
function closeModal(id){document.getElementById(id).classList.add('hidden');}
|
||||
document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.add('hidden');}));
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
async function init(){
|
||||
if(!token){return;}
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
loadDashboard();
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
// Auto-login if token exists
|
||||
if(token) init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>وبلاگ | دکتر سوسن آلطه</title>
|
||||
<meta name="description" content="مقالات تخصصی دکتر سوسن آلطه درباره زیبایی پوست، بوتاکس، فیلر، لیزر و مراقبت از پوست."/>
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--gold:#B8955A;--gold-l:#D4B483;--gold-pale:#F5ECD8;--bg:#FAFAF7;--white:#fff;--dark:#1E1E1E;--mid:#5A5A5A;--light:#9A9A9A;--border:#E8E2D9}
|
||||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--dark);direction:rtl;line-height:1.8}
|
||||
a{text-decoration:none;color:inherit}
|
||||
|
||||
/* Navbar */
|
||||
header{position:sticky;top:0;z-index:50;background:rgba(250,250,247,.92);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;height:70px;display:flex;align-items:center;justify-content:space-between}
|
||||
.logo{font-size:1rem;font-weight:600;color:var(--gold)}
|
||||
nav{display:flex;gap:2rem}
|
||||
nav a{color:var(--mid);font-size:.88rem;transition:color .2s}
|
||||
nav a:hover{color:var(--gold)}
|
||||
.nav-cta{background:var(--gold);color:#fff!important;padding:.4rem 1.1rem;border-radius:50px;font-size:.85rem!important}
|
||||
|
||||
/* Hero */
|
||||
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:4rem 2rem 3rem;text-align:center}
|
||||
.blog-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
||||
.blog-hero p{font-size:1rem;color:var(--mid);max-width:520px;margin:0 auto}
|
||||
|
||||
/* Filter */
|
||||
.filter-bar{max-width:1100px;margin:2rem auto 0;padding:0 2rem;display:flex;gap:.6rem;flex-wrap:wrap}
|
||||
.filter-btn{background:transparent;border:1.5px solid var(--border);color:var(--mid);padding:.4rem 1.1rem;border-radius:50px;font-family:inherit;font-size:.85rem;cursor:pointer;transition:all .2s}
|
||||
.filter-btn.active,.filter-btn:hover{background:var(--gold);border-color:var(--gold);color:#fff}
|
||||
|
||||
/* Grid */
|
||||
.blog-grid{max-width:1100px;margin:2rem auto;padding:0 2rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem}
|
||||
@media(max-width:900px){.blog-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:600px){.blog-grid{grid-template-columns:1fr}}
|
||||
|
||||
.post-card{background:var(--white);border-radius:16px;border:1px solid var(--border);overflow:hidden;transition:transform .3s,box-shadow .3s;display:flex;flex-direction:column}
|
||||
.post-card:hover{transform:translateY(-4px);box-shadow:0 12px 40px rgba(184,149,90,.15)}
|
||||
.post-card-img{aspect-ratio:16/9;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:2rem}
|
||||
.post-card-img img{width:100%;height:100%;object-fit:cover}
|
||||
.post-card-body{padding:1.3rem;flex:1;display:flex;flex-direction:column;gap:.6rem}
|
||||
.post-cat{font-size:.72rem;font-weight:600;color:var(--gold);background:var(--gold-pale);padding:.2rem .7rem;border-radius:50px;display:inline-block}
|
||||
.post-title{font-size:1rem;font-weight:600;color:var(--dark);line-height:1.5}
|
||||
.post-title:hover{color:var(--gold)}
|
||||
.post-excerpt{font-size:.85rem;color:var(--mid);line-height:1.7;flex:1}
|
||||
.post-meta{display:flex;align-items:center;justify-content:space-between;font-size:.75rem;color:var(--light);margin-top:auto;padding-top:.6rem;border-top:1px solid var(--border)}
|
||||
.read-more{color:var(--gold);font-weight:500;font-size:.82rem}
|
||||
|
||||
/* Pagination */
|
||||
.pagination{display:flex;gap:.5rem;justify-content:center;padding:2rem;margin-top:1rem}
|
||||
.page-btn{width:38px;height:38px;border-radius:8px;border:1.5px solid var(--border);background:transparent;font-family:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
||||
.page-btn.active,.page-btn:hover{background:var(--gold);border-color:var(--gold);color:#fff}
|
||||
|
||||
/* Empty */
|
||||
.empty{text-align:center;padding:4rem 2rem;color:var(--light)}
|
||||
.empty svg{width:48px;height:48px;margin:0 auto 1rem}
|
||||
|
||||
/* Search */
|
||||
.search-wrap{max-width:480px;margin:0 auto;position:relative}
|
||||
.search-wrap input{width:100%;border:1.5px solid var(--border);border-radius:50px;padding:.65rem 1.2rem .65rem 3rem;font-family:inherit;font-size:.9rem;direction:rtl;outline:none;background:var(--white);transition:border-color .2s}
|
||||
.search-wrap input:focus{border-color:var(--gold)}
|
||||
.search-wrap svg{position:absolute;left:1rem;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--light)}
|
||||
|
||||
footer{background:var(--dark);color:rgba(255,255,255,.5);text-align:center;padding:2rem;font-size:.83rem;margin-top:3rem}
|
||||
footer a{color:var(--gold-l)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a class="logo" href="index.html">دکتر سوسن آلطه</a>
|
||||
<nav>
|
||||
<a href="index.html">خانه</a>
|
||||
<a href="index.html#services">خدمات</a>
|
||||
<a href="blog.html" style="color:var(--gold)">وبلاگ</a>
|
||||
<a href="index.html#contact" class="nav-cta">رزرو نوبت</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="blog-hero">
|
||||
<h1>وبلاگ تخصصی پوست و زیبایی</h1>
|
||||
<p>آخرین مقالات و راهنماهای تخصصی درباره مراقبت از پوست، زیبایی و درمانهای تخصصی</p>
|
||||
<div class="search-wrap" style="margin-top:1.5rem">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="searchInput" placeholder="جستجو در مقالات..." oninput="handleSearch()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar" id="filterBar">
|
||||
<button class="filter-btn active" onclick="filterCat('',this)">همه</button>
|
||||
</div>
|
||||
|
||||
<div class="blog-grid" id="blogGrid">
|
||||
<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16"/><path d="M20 8H8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2z"/></svg><p>در حال بارگذاری...</p></div>
|
||||
</div>
|
||||
|
||||
<div class="pagination" id="pagination"></div>
|
||||
|
||||
<footer>
|
||||
<p>© ۱۴۰۳ دکتر سوسن آلطه | <a href="admin/index.html">پنل مدیریت</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const API='http://localhost:5000';
|
||||
let currentCat='', currentSearch='', currentPage=1, pageSize=9, totalPosts=0;
|
||||
|
||||
async function loadPosts(){
|
||||
const params=new URLSearchParams({page:currentPage,pageSize});
|
||||
if(currentCat) params.set('category',currentCat);
|
||||
if(currentSearch) params.set('search',currentSearch);
|
||||
const data=await fetch(`${API}/api/blog/posts?${params}`).then(r=>r.json()).catch(()=>null);
|
||||
if(!data){renderError();return;}
|
||||
totalPosts=data.total;
|
||||
renderGrid(data.items);
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
function renderGrid(posts){
|
||||
const g=document.getElementById('blogGrid');
|
||||
if(!posts.length){g.innerHTML='<div class="empty"><p>مقالهای یافت نشد</p></div>';return;}
|
||||
g.innerHTML=posts.map(p=>`
|
||||
<div class="post-card">
|
||||
<div class="post-card-img">${p.featuredImage?`<img src="${p.featuredImage}" alt="${p.title}" loading="lazy"/>`:'📝'}</div>
|
||||
<div class="post-card-body">
|
||||
<span class="post-cat">${p.category?.name||'عمومی'}</span>
|
||||
<a href="post.html?slug=${p.slug}" class="post-title">${p.title}</a>
|
||||
<p class="post-excerpt">${p.excerpt.substring(0,120)}...</p>
|
||||
<div class="post-meta">
|
||||
<span>🕐 ${p.readingTimeMinutes} دقیقه | 👁 ${p.viewCount}</span>
|
||||
<a href="post.html?slug=${p.slug}" class="read-more">ادامه مطلب ←</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function renderPagination(){
|
||||
const total=Math.ceil(totalPosts/pageSize);
|
||||
const pg=document.getElementById('pagination');
|
||||
if(total<=1){pg.innerHTML='';return;}
|
||||
let h='';
|
||||
for(let i=1;i<=total;i++) h+=`<button class="page-btn${i===currentPage?' active':''}" onclick="gotoPage(${i})">${i}</button>`;
|
||||
pg.innerHTML=h;
|
||||
}
|
||||
|
||||
function renderError(){document.getElementById('blogGrid').innerHTML='<div class="empty"><p>خطا در اتصال به سرور. مطمئن شوید API روی پورت ۵۰۰۰ اجرا است.</p></div>';}
|
||||
|
||||
function filterCat(slug,btn){
|
||||
currentCat=slug; currentPage=1;
|
||||
document.querySelectorAll('.filter-btn').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
loadPosts();
|
||||
}
|
||||
|
||||
let searchTimer;
|
||||
function handleSearch(){
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer=setTimeout(()=>{currentSearch=document.getElementById('searchInput').value;currentPage=1;loadPosts();},400);
|
||||
}
|
||||
|
||||
function gotoPage(p){currentPage=p;loadPosts();window.scrollTo({top:200,behavior:'smooth'});}
|
||||
|
||||
async function loadCategories(){
|
||||
const cats=await fetch(`${API}/api/blog/categories`).then(r=>r.json()).catch(()=>[]);
|
||||
const bar=document.getElementById('filterBar');
|
||||
cats.forEach(c=>{
|
||||
const btn=document.createElement('button');
|
||||
btn.className='filter-btn';
|
||||
btn.textContent=`${c.name} (${c.postCount})`;
|
||||
btn.onclick=()=>filterCat(c.slug,btn);
|
||||
bar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
loadCategories();
|
||||
loadPosts();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"insecure-registries": ["171.22.25.73:8087"],
|
||||
"registry-mirrors": [
|
||||
"https://docker-mirror.liara.ir",
|
||||
"https://ghcr-mirror.liara.ir",
|
||||
"https://mcr-mirror.liara.ir"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
|
||||
# ── .NET API + Razor Pages + Static Files ────────────────────────────────────
|
||||
api:
|
||||
image: 171.22.25.73:8087/drsousan/api:${API_TAG:-latest}
|
||||
build: # used by local: docker compose build
|
||||
context: ./DrSousan.Api
|
||||
dockerfile: Dockerfile
|
||||
container_name: drsousan_api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HOST_PORT:-5000}:8080" # http://localhost:5000
|
||||
volumes:
|
||||
- db_data:/data # SQLite database (persistent)
|
||||
- uploads_data:/app/wwwroot/uploads # user-uploaded images (persistent)
|
||||
environment:
|
||||
# Database — SQLite stored on /data volume
|
||||
ConnectionStrings__Default: "Data Source=/data/drsousan.db"
|
||||
|
||||
# JWT — CHANGE Jwt__Key in production (min 32 chars)
|
||||
Jwt__Key: "${JWT_KEY:-DrSousanSecretKey2024!ChangeThisInProduction!MinLength32Chars}"
|
||||
Jwt__Issuer: "${JWT_ISSUER:-DrSousanApi}"
|
||||
Jwt__Audience: "${JWT_AUDIENCE:-DrSousanAdmin}"
|
||||
|
||||
# Admin login — override via .env file in production
|
||||
Admin__Username: "${ADMIN_USERNAME:-admin}"
|
||||
Admin__Password: "${ADMIN_PASSWORD:-admin123}"
|
||||
|
||||
ASPNETCORE_ENVIRONMENT: "Production"
|
||||
|
||||
# ── Named Volumes ────────────────────────────────────────────────────────────
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
uploads_data:
|
||||
driver: local
|
||||
+1483
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<!-- SEO meta tags injected by JS -->
|
||||
<title id="pageTitle">مقاله | دکتر سوسن آلطه</title>
|
||||
<meta id="metaDesc" name="description" content=""/>
|
||||
<meta id="metaKw" name="keywords" content=""/>
|
||||
<!-- Open Graph -->
|
||||
<meta id="ogTitle" property="og:title" content=""/>
|
||||
<meta id="ogDesc" property="og:description" content=""/>
|
||||
<meta id="ogImg" property="og:image" content=""/>
|
||||
<meta property="og:type" content="article"/>
|
||||
<meta property="og:locale" content="fa_IR"/>
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
<meta id="twTitle" name="twitter:title" content=""/>
|
||||
<meta id="twDesc" name="twitter:description" content=""/>
|
||||
<!-- Canonical -->
|
||||
<link id="canonical" rel="canonical" href=""/>
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--gold:#B8955A;--gold-l:#D4B483;--gold-pale:#F5ECD8;--bg:#FAFAF7;--white:#fff;--dark:#1E1E1E;--mid:#5A5A5A;--light:#9A9A9A;--border:#E8E2D9}
|
||||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--dark);direction:rtl;line-height:1.8}
|
||||
a{text-decoration:none;color:inherit}
|
||||
|
||||
header{position:sticky;top:0;z-index:50;background:rgba(250,250,247,.92);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;height:70px;display:flex;align-items:center;justify-content:space-between}
|
||||
.logo{font-size:1rem;font-weight:600;color:var(--gold)}
|
||||
nav{display:flex;gap:2rem}
|
||||
nav a{color:var(--mid);font-size:.88rem;transition:color .2s}
|
||||
nav a:hover{color:var(--gold)}
|
||||
.nav-cta{background:var(--gold);color:#fff!important;padding:.4rem 1.1rem;border-radius:50px;font-size:.85rem!important}
|
||||
|
||||
/* Layout */
|
||||
.post-layout{max-width:1100px;margin:0 auto;padding:3rem 2rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
||||
@media(max-width:900px){.post-layout{grid-template-columns:1fr}}
|
||||
|
||||
/* Article */
|
||||
.article-hero{border-radius:16px;overflow:hidden;margin-bottom:2rem;aspect-ratio:16/6;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;font-size:3rem}
|
||||
.article-hero img{width:100%;height:100%;object-fit:cover}
|
||||
.article-cat{display:inline-block;background:var(--gold-pale);color:var(--gold);font-size:.75rem;font-weight:600;padding:.25rem .8rem;border-radius:50px;margin-bottom:.8rem}
|
||||
.article-title{font-size:clamp(1.5rem,3vw,2rem);font-weight:700;line-height:1.4;margin-bottom:1rem}
|
||||
.article-meta{display:flex;gap:1.5rem;font-size:.8rem;color:var(--light);margin-bottom:2rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||||
.article-meta span{display:flex;align-items:center;gap:.3rem}
|
||||
|
||||
/* Content */
|
||||
.article-content{font-size:.95rem;line-height:2;color:var(--dark)}
|
||||
.article-content h2{font-size:1.3rem;font-weight:700;margin:1.8rem 0 .8rem;color:var(--dark);padding-bottom:.4rem;border-bottom:2px solid var(--gold-pale)}
|
||||
.article-content h3{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .6rem;color:var(--dark)}
|
||||
.article-content p{margin-bottom:1rem}
|
||||
.article-content ul,.article-content ol{padding-right:1.5rem;margin-bottom:1rem}
|
||||
.article-content li{margin-bottom:.4rem}
|
||||
.article-content strong{color:var(--dark);font-weight:600}
|
||||
.article-content a{color:var(--gold);border-bottom:1px solid var(--gold-pale)}
|
||||
.article-content blockquote{border-right:4px solid var(--gold);padding:.8rem 1.2rem;background:var(--gold-pale);border-radius:0 8px 8px 0;margin:1.2rem 0;font-style:italic;color:var(--mid)}
|
||||
|
||||
/* Tags */
|
||||
.article-tags{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--border);display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.tag{background:var(--bg);border:1px solid var(--border);padding:.25rem .75rem;border-radius:50px;font-size:.78rem;color:var(--mid)}
|
||||
|
||||
/* Share */
|
||||
.share-box{margin-top:2rem;padding:1.5rem;background:var(--white);border-radius:12px;border:1px solid var(--border);text-align:center}
|
||||
.share-title{font-size:.9rem;font-weight:600;margin-bottom:1rem}
|
||||
.share-btns{display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
|
||||
.share-btn{padding:.45rem 1rem;border-radius:8px;font-family:inherit;font-size:.82rem;cursor:pointer;border:none;font-weight:500}
|
||||
.share-telegram{background:#2CA5E0;color:#fff}
|
||||
.share-whatsapp{background:#25D366;color:#fff}
|
||||
.share-copy{background:var(--bg);color:var(--mid);border:1px solid var(--border)}
|
||||
|
||||
/* CTA */
|
||||
.cta-box{margin-top:2.5rem;padding:2rem;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);border-radius:16px;text-align:center}
|
||||
.cta-box h3{font-size:1.1rem;font-weight:700;margin-bottom:.6rem}
|
||||
.cta-box p{font-size:.88rem;color:var(--mid);margin-bottom:1.2rem}
|
||||
.cta-btn{background:var(--gold);color:#fff;padding:.7rem 1.8rem;border-radius:50px;font-family:inherit;font-size:.9rem;font-weight:600;border:none;cursor:pointer;display:inline-block}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar{position:sticky;top:90px}
|
||||
.sidebar-card{background:var(--white);border-radius:14px;border:1px solid var(--border);padding:1.4rem;margin-bottom:1.5rem}
|
||||
.sidebar-title{font-size:.88rem;font-weight:700;color:var(--dark);margin-bottom:1rem;padding-bottom:.6rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post{display:flex;gap:.8rem;margin-bottom:.9rem;padding-bottom:.9rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}
|
||||
.recent-img{width:56px;height:56px;border-radius:8px;background:var(--gold-pale);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:1.3rem}
|
||||
.recent-img img{width:100%;height:100%;object-fit:cover;border-radius:8px}
|
||||
.recent-title{font-size:.82rem;font-weight:600;line-height:1.4;color:var(--dark)}
|
||||
.recent-title:hover{color:var(--gold)}
|
||||
.recent-date{font-size:.73rem;color:var(--light);margin-top:.2rem}
|
||||
.doctor-card{text-align:center}
|
||||
.doc-avatar{width:80px;height:80px;border-radius:50%;background:var(--gold-pale);margin:0 auto .8rem;display:flex;align-items:center;justify-content:center;font-size:2rem}
|
||||
.doc-name{font-size:.95rem;font-weight:700;color:var(--dark)}
|
||||
.doc-title{font-size:.78rem;color:var(--light);margin:.2rem 0 .8rem}
|
||||
.doc-btn{background:var(--gold);color:#fff;padding:.5rem 1.2rem;border-radius:50px;font-family:inherit;font-size:.82rem;border:none;cursor:pointer;width:100%}
|
||||
|
||||
/* Loading */
|
||||
.loading{text-align:center;padding:5rem 2rem;color:var(--light)}
|
||||
|
||||
footer{background:var(--dark);color:rgba(255,255,255,.5);text-align:center;padding:2rem;font-size:.83rem;margin-top:3rem}
|
||||
footer a{color:var(--gold-l)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a class="logo" href="index.html">دکتر سوسن آلطه</a>
|
||||
<nav>
|
||||
<a href="index.html">خانه</a>
|
||||
<a href="blog.html">وبلاگ</a>
|
||||
<a href="index.html#contact" class="nav-cta">رزرو نوبت</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div id="loadingState" class="loading"><p>در حال بارگذاری...</p></div>
|
||||
|
||||
<div id="postLayout" class="post-layout" style="display:none">
|
||||
<!-- Article -->
|
||||
<article>
|
||||
<div class="article-hero" id="articleHero">📝</div>
|
||||
<span class="article-cat" id="articleCat">زیبایی پوست</span>
|
||||
<h1 class="article-title" id="articleTitle"></h1>
|
||||
<div class="article-meta">
|
||||
<span>✍️ <span id="articleAuthor">دکتر سوسن آلطه</span></span>
|
||||
<span>📅 <span id="articleDate"></span></span>
|
||||
<span>🕐 <span id="articleTime"></span> دقیقه مطالعه</span>
|
||||
<span>👁 <span id="articleViews"></span> بازدید</span>
|
||||
</div>
|
||||
<div class="article-content" id="articleContent"></div>
|
||||
<div class="article-tags" id="articleTags"></div>
|
||||
<div class="share-box">
|
||||
<div class="share-title">این مقاله را به اشتراک بگذارید</div>
|
||||
<div class="share-btns">
|
||||
<button class="share-btn share-telegram" onclick="shareTo('telegram')">📱 تلگرام</button>
|
||||
<button class="share-btn share-whatsapp" onclick="shareTo('whatsapp')">💬 واتساپ</button>
|
||||
<button class="share-btn share-copy" onclick="copyLink()">🔗 کپی لینک</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cta-box">
|
||||
<h3>آماده تحول در پوستتان هستید؟</h3>
|
||||
<p>همین امروز با دکتر سوسن آلطه مشاوره رایگان دریافت کنید</p>
|
||||
<a href="index.html#contact" class="cta-btn">رزرو نوبت رایگان</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-card doctor-card">
|
||||
<div class="doc-avatar">👩⚕️</div>
|
||||
<div class="doc-name">دکتر سوسن آلطه</div>
|
||||
<div class="doc-title">پزشک عمومی | متخصص زیبایی پوست</div>
|
||||
<button class="doc-btn" onclick="location.href='index.html#contact'">رزرو نوبت</button>
|
||||
</div>
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">مقالات اخیر</div>
|
||||
<div id="recentPosts"></div>
|
||||
</div>
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">خدمات ما</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.5rem;font-size:.85rem">
|
||||
<a href="index.html#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> بوتاکس و فیلر</a>
|
||||
<a href="index.html#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> لیزر درمانی</a>
|
||||
<a href="index.html#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مزوتراپی</a>
|
||||
<a href="index.html#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> پاکسازی پوست</a>
|
||||
<a href="index.html#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مشاوره زیبایی</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div id="errorState" style="display:none;text-align:center;padding:5rem 2rem">
|
||||
<p style="color:#999;font-size:1.1rem">مقاله یافت نشد</p>
|
||||
<a href="blog.html" style="color:var(--gold);margin-top:1rem;display:inline-block">← بازگشت به وبلاگ</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© ۱۴۰۳ دکتر سوسن آلطه | <a href="blog.html">وبلاگ</a> | <a href="admin/index.html">پنل مدیریت</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const API='http://localhost:5000';
|
||||
const slug=new URLSearchParams(location.search).get('slug');
|
||||
|
||||
function fa(n){return String(n).replace(/\d/g,d=>'۰۱۲۳۴۵۶۷۸۹'[d]);}
|
||||
function fmtDate(d){if(!d)return'';const dt=new Date(d);const months=['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند'];const m=months[dt.getMonth()]||'';return `${fa(dt.getDate())} ${m} ${fa(dt.getFullYear())}`;}
|
||||
|
||||
async function loadPost(){
|
||||
if(!slug){showError();return;}
|
||||
try{
|
||||
const post=await fetch(`${API}/api/blog/posts/${slug}`).then(r=>{if(!r.ok)throw r;return r.json();});
|
||||
renderPost(post);
|
||||
injectSeo(post);
|
||||
loadRecent(post.id);
|
||||
}catch(e){showError();}
|
||||
}
|
||||
|
||||
function renderPost(p){
|
||||
document.getElementById('loadingState').style.display='none';
|
||||
document.getElementById('postLayout').style.display='grid';
|
||||
|
||||
if(p.featuredImage) document.getElementById('articleHero').innerHTML=`<img src="${p.featuredImage}" alt="${p.title}" loading="lazy"/>`;
|
||||
document.getElementById('articleCat').textContent=p.category?.name||'عمومی';
|
||||
document.getElementById('articleTitle').textContent=p.title;
|
||||
document.getElementById('articleAuthor').textContent=p.author;
|
||||
document.getElementById('articleDate').textContent=fmtDate(p.publishedAt);
|
||||
document.getElementById('articleTime').textContent=fa(p.readingTimeMinutes);
|
||||
document.getElementById('articleViews').textContent=fa(p.viewCount);
|
||||
document.getElementById('articleContent').innerHTML=p.content;
|
||||
|
||||
if(p.keywords){
|
||||
document.getElementById('articleTags').innerHTML=
|
||||
p.keywords.split(',').filter(Boolean).map(k=>`<span class="tag">${k.trim()}</span>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function injectSeo(p){
|
||||
document.getElementById('pageTitle').textContent=p.metaTitle||p.title+' | دکتر سوسن آلطه';
|
||||
setMeta('metaDesc',p.metaDescription);
|
||||
setMeta('metaKw',p.keywords);
|
||||
setMeta('ogTitle',p.metaTitle||p.title);
|
||||
setMeta('ogDesc',p.metaDescription);
|
||||
setMeta('ogImg',p.ogImage||p.featuredImage);
|
||||
setMeta('twTitle',p.metaTitle||p.title);
|
||||
setMeta('twDesc',p.metaDescription);
|
||||
document.getElementById('canonical').href=location.href;
|
||||
|
||||
// JSON-LD structured data
|
||||
const schema={
|
||||
"@context":"https://schema.org",
|
||||
"@type":p.articleType||"Article",
|
||||
"headline":p.title,
|
||||
"description":p.excerpt,
|
||||
"author":{"@type":"Person","name":p.author||"دکتر سوسن آلطه"},
|
||||
"publisher":{"@type":"Organization","name":"دکتر سوسن آلطه"},
|
||||
"datePublished":p.publishedAt,
|
||||
"dateModified":p.updatedAt,
|
||||
"image":p.featuredImage||"",
|
||||
"keywords":p.keywords
|
||||
};
|
||||
const s=document.createElement('script');
|
||||
s.type='application/ld+json';
|
||||
s.textContent=JSON.stringify(schema);
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function setMeta(id,val){const el=document.getElementById(id);if(el&&val)el.setAttribute(el.hasAttribute('content')?'content':'href',val);}
|
||||
|
||||
async function loadRecent(currentId){
|
||||
const data=await fetch(`${API}/api/blog/posts?page=1&pageSize=5`).then(r=>r.json()).catch(()=>null);
|
||||
if(!data) return;
|
||||
const rp=document.getElementById('recentPosts');
|
||||
rp.innerHTML=data.items.filter(p=>p.id!==currentId).slice(0,4).map(p=>`
|
||||
<div class="recent-post">
|
||||
<div class="recent-img">${p.featuredImage?`<img src="${p.featuredImage}" alt="${p.title}" loading="lazy"/>`:'📝'}</div>
|
||||
<div><a href="post.html?slug=${p.slug}" class="recent-title">${p.title}</a><div class="recent-date">🕐 ${p.readingTimeMinutes} دقیقه</div></div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function showError(){
|
||||
document.getElementById('loadingState').style.display='none';
|
||||
document.getElementById('errorState').style.display='block';
|
||||
}
|
||||
|
||||
function shareTo(platform){
|
||||
const url=encodeURIComponent(location.href);
|
||||
const title=encodeURIComponent(document.title);
|
||||
const links={telegram:`https://t.me/share/url?url=${url}&text=${title}`,whatsapp:`https://wa.me/?text=${title}%20${url}`};
|
||||
window.open(links[platform],'_blank');
|
||||
}
|
||||
|
||||
function copyLink(){
|
||||
navigator.clipboard.writeText(location.href).then(()=>{
|
||||
const btn=event.target;btn.textContent='✓ کپی شد!';setTimeout(()=>btn.textContent='🔗 کپی لینک',2000);
|
||||
});
|
||||
}
|
||||
|
||||
loadPost();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
echo ╔════════════════════════════════════════════════╗
|
||||
echo ║ Dr. Sousan Website ║
|
||||
echo ║ Site: http://localhost:5000/ ║
|
||||
echo ║ Blog: http://localhost:5000/blog ║
|
||||
echo ║ Admin: http://localhost:5000/admin/ ║
|
||||
echo ╚════════════════════════════════════════════════╝
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0DrSousan.Api"
|
||||
dotnet run --configuration Development
|
||||
Reference in New Issue
Block a user