Migrate to Blazor WASM, MSSQL platform DB, and durable audio PVC.
build-and-deploy / deploy (push) Failing after 15m11s

Remove in-cluster Postgres; serve WASM via nginx; add audio import job for Gooya archives.
This commit is contained in:
2026-10-09 22:40:53 +03:30
parent 7f06856f45
commit ae746d65ba
32 changed files with 760 additions and 525 deletions
@@ -56,23 +56,33 @@ public static class TafsirEndpoints
return Results.Ok(detail);
});
group.MapGet("/audio/{episodeId:guid}", async (Guid episodeId, AppDbContext db, IWebHostEnvironment env, CancellationToken ct) =>
group.MapGet("/audio/{episodeId:guid}", async (Guid episodeId, AppDbContext db, IConfiguration config, IWebHostEnvironment env, CancellationToken ct) =>
{
var episode = await db.Episodes.AsNoTracking().FirstOrDefaultAsync(e => e.Id == episodeId, ct);
if (episode is null) return Results.NotFound();
var relative = episode.AudioUrl.TrimStart('/');
var path = Path.Combine(env.ContentRootPath, "wwwroot", relative);
if (!File.Exists(path))
var audioRoot = config["AudioRoot"] ?? "/data/audio";
var relative = episode.AudioUrl.TrimStart('/').Replace('\\', '/');
// Prefer PVC mount; fallback to wwwroot for local sample files
var candidates = new[]
{
// Fallback placeholder: return empty audio-friendly response path for demo
var placeholder = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
if (File.Exists(placeholder))
return Results.File(placeholder, "audio/mpeg", enableRangeProcessing: true);
return Results.NotFound();
Path.Combine(audioRoot, relative),
Path.Combine(env.ContentRootPath, "wwwroot", relative),
Path.Combine(env.ContentRootPath, "wwwroot", "audio", Path.GetFileName(relative))
};
foreach (var path in candidates)
{
if (File.Exists(path))
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
}
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
var silence = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
if (File.Exists(silence))
return Results.File(silence, "audio/mpeg", enableRangeProcessing: true);
return Results.NotFound();
});
return group;
+2 -1
View File
@@ -47,7 +47,8 @@ using (var scope = app.Services.CreateScope())
{
try
{
await DbSeeder.SeedAsync(db);
var audioRoot = app.Configuration["AudioRoot"] ?? "/data/audio";
await DbSeeder.SeedAsync(db, audioRoot);
break;
}
catch (Exception ex) when (attempt < 30)
+2 -1
View File
@@ -1,7 +1,8 @@
{
"ConnectionStrings": {
"Default": "Host=localhost;Port=5434;Database=tafsirquran;Username=tafsir;Password=tafsir_dev"
"Default": "Server=localhost,31433;Database=TafsirQuran;User Id=sa;Password=87zH26nbqT;TrustServerCertificate=True"
},
"AudioRoot": "/data/audio",
"Jwt": {
"Key": "TafsirQuran-Dev-Secret-Key-Change-In-Production-32+",
"Issuer": "TafsirQuran",
@@ -10,10 +10,10 @@ public static class DependencyInjection
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("Default")
?? "Host=localhost;Port=5434;Database=tafsirquran;Username=tafsir;Password=tafsir_dev";
?? "Server=localhost,31433;Database=TafsirQuran;User Id=sa;Password=87zH26nbqT;TrustServerCertificate=True";
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
options.UseSqlServer(connectionString));
return services;
}
@@ -7,114 +7,154 @@ public static class DbSeeder
{
public static readonly Guid GhatrehChekaniId = Guid.Parse("11111111-1111-1111-1111-111111111111");
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
private static readonly (string Ar, string Fa)[] SurahNames =
[
("الفاتحة", "فاتحه"), ("البقرة", "بقره"), ("آل عمران", "آل عمران"), ("النساء", "نساء"),
("المائدة", "مائده"), ("الأنعام", "انعام"), ("الأعراف", "اعراف"), ("الأنفال", "انفال"),
("التوبة", "توبه"), ("يونس", "یونس"), ("هود", "هود"), ("يوسف", "یوسف"),
("الرعد", "رعد"), ("إبراهيم", "ابراهیم"), ("الحجر", "حجر"), ("النحل", "نحل"),
("الإسراء", "اسراء"), ("الكهف", "کهف"), ("مريم", "مریم"), ("طه", "طه"),
("الأنبياء", "انبیاء"), ("الحج", "حج"), ("المؤمنون", "مؤمنون"), ("النور", "نور"),
("الفرقان", "فرقان"), ("الشعراء", "شعراء"), ("النمل", "نمل"), ("القصص", "قصص"),
("العنكبوت", "عنکبوت"), ("الروم", "روم"), ("لقمان", "لقمان"), ("السجدة", "سجده"),
("الأحزاب", "احزاب"), ("سبأ", "سبأ"), ("فاطر", "فاطر"), ("يس", "یس"),
("الصافات", "صافات"), ("ص", "ص"), ("الزمر", "زمر"), ("غافر", "غافر"),
("فصلت", "فصلت"), ("الشورى", "شوری"), ("الزخرف", "زخرف"), ("الدخان", "دخان"),
("الجاثية", "جاثیه"), ("الأحقاف", "احقاف"), ("محمد", "محمد"), ("الفتح", "فتح"),
("الحجرات", "حجرات"), ("ق", "ق"), ("الذاريات", "ذاریات"), ("الطور", "طور"),
("النجم", "نجم"), ("القمر", "قمر"), ("الرحمن", "رحمن"), ("الواقعة", "واقعه"),
("الحديد", "حدید"), ("المجادلة", "مجادله"), ("الحشر", "حشر"), ("الممتحنة", "ممتحنه"),
("الصف", "صف"), ("الجمعة", "جمعه"), ("المنافقون", "منافقون"), ("التغابن", "تغابن"),
("الطلاق", "طلاق"), ("التحريم", "تحریم"), ("الملك", "ملک"), ("القلم", "قلم"),
("الحاقة", "حاقه"), ("المعارج", "معارج"), ("نوح", "نوح"), ("الجن", "جن"),
("المزمل", "مزمل"), ("المدثر", "مدثر"), ("القيامة", "قیامه"), ("الإنسان", "انسان"),
("المرسلات", "مرسلات"), ("النبأ", "نبأ"), ("النازعات", "نازعات"), ("عبس", "عبس"),
("التكوير", "تکویر"), ("الانفطار", "انفطار"), ("المطففين", "مطففین"), ("الانشقاق", "انشقاق"),
("البروج", "بروج"), ("الطارق", "طارق"), ("الأعلى", "اعلی"), ("الغاشية", "غاشیه"),
("الفجر", "فجر"), ("البلد", "بلد"), ("الشمس", "شمس"), ("الليل", "لیل"),
("الضحى", "ضحی"), ("الشرح", "شرح"), ("التين", "تین"), ("العلق", "علق"),
("القدر", "قدر"), ("البينة", "بینه"), ("الزلزلة", "زلزله"), ("العاديات", "عادیات"),
("القارعة", "قارعه"), ("التكاثر", "تکاثر"), ("العصر", "عصر"), ("الهمزة", "همزه"),
("الفيل", "فیل"), ("قريش", "قریش"), ("الماعون", "ماعون"), ("الكوثر", "کوثر"),
("الكافرون", "کافرون"), ("النصر", "نصر"), ("المسد", "مسد"), ("الإخلاص", "اخلاص"),
("الفلق", "فلق"), ("الناس", "ناس")
];
public static async Task SeedAsync(AppDbContext db, string? audioRoot = null, CancellationToken ct = default)
{
await db.Database.MigrateAsync(ct);
if (!await db.Surahs.AnyAsync(ct))
{
db.Surahs.AddRange(
new Surah { Id = 1, Number = 1, NameAr = "الفاتحة", NameFa = "فاتحه" },
new Surah { Id = 2, Number = 2, NameAr = "البقرة", NameFa = "بقره" },
new Surah { Id = 112, Number = 112, NameAr = "الإخلاص", NameFa = "اخلاص" }
);
db.Ayahs.AddRange(
new Ayah
for (var i = 0; i < SurahNames.Length; i++)
{
var n = i + 1;
db.Surahs.Add(new Surah
{
SurahId = 1,
Number = 1,
TextAr = "بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ",
TextFa = "به نام خداوند بخشنده مهربان"
},
new Ayah
{
SurahId = 1,
Number = 2,
TextAr = "الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ",
TextFa = "ستایش مخصوص خداوندی است که پروردگار جهانیان است"
},
new Ayah
{
SurahId = 1,
Number = 3,
TextAr = "الرَّحْمَٰنِ الرَّحِيمِ",
TextFa = "بخشنده مهربان"
},
new Ayah
{
SurahId = 112,
Number = 1,
TextAr = "قُلْ هُوَ اللَّهُ أَحَدٌ",
TextFa = "بگو اوست خدای یگانه"
}
);
Id = n,
Number = n,
NameAr = SurahNames[i].Ar,
NameFa = SurahNames[i].Fa
});
}
await db.SaveChangesAsync(ct);
}
if (!await db.Tafsirs.AnyAsync(ct))
{
var tafsir = new Tafsir
db.Tafsirs.Add(new Tafsir
{
Id = GhatrehChekaniId,
Title = "تفسیر قطره چکانی",
Author = "قرائتی",
Slug = "ghatreh-chekani",
IsPublished = true
};
db.Tafsirs.Add(tafsir);
db.Episodes.AddRange(
new TafsirEpisode
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222201"),
TafsirId = GhatrehChekaniId,
SurahId = 1,
AyahNumber = 1,
Title = "فاتحه آیه ۱",
AudioUrl = "/audio/sample-1.mp3",
DurationSeconds = 180,
SortOrder = 1,
Summary = "تفسیر قطره‌چکانی سوره فاتحه آیه اول"
},
new TafsirEpisode
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222202"),
TafsirId = GhatrehChekaniId,
SurahId = 1,
AyahNumber = 2,
Title = "فاتحه آیه ۲",
AudioUrl = "/audio/sample-2.mp3",
DurationSeconds = 210,
SortOrder = 2,
Summary = "تفسیر قطره‌چکانی سوره فاتحه آیه دوم"
},
new TafsirEpisode
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222203"),
TafsirId = GhatrehChekaniId,
SurahId = 1,
AyahNumber = 3,
Title = "فاتحه آیه ۳",
AudioUrl = "/audio/sample-3.mp3",
DurationSeconds = 195,
SortOrder = 3,
Summary = "تفسیر قطره‌چکانی سوره فاتحه آیه سوم"
},
new TafsirEpisode
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222204"),
TafsirId = GhatrehChekaniId,
SurahId = 112,
AyahNumber = 1,
Title = "اخلاص آیه ۱",
AudioUrl = "/audio/sample-4.mp3",
DurationSeconds = 160,
SortOrder = 4,
Summary = "تفسیر قطره‌چکانی سوره اخلاص آیه اول"
}
);
});
await db.SaveChangesAsync(ct);
}
await db.SaveChangesAsync(ct);
await SyncEpisodesFromAudioAsync(db, audioRoot, ct);
}
public static async Task SyncEpisodesFromAudioAsync(AppDbContext db, string? audioRoot, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(audioRoot) || !Directory.Exists(audioRoot))
return;
var tafsir = await db.Tafsirs.FirstOrDefaultAsync(t => t.Id == GhatrehChekaniId, ct);
if (tafsir is null) return;
var existing = await db.Episodes
.Where(e => e.TafsirId == GhatrehChekaniId)
.Select(e => e.AudioUrl)
.ToListAsync(ct);
var existingSet = existing.ToHashSet(StringComparer.OrdinalIgnoreCase);
var audioFiles = Directory.EnumerateFiles(audioRoot, "*.*", SearchOption.AllDirectories)
.Where(f => f.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".m4a", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".ogg", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList();
if (audioFiles.Count == 0) return;
var sort = existing.Count;
var added = 0;
foreach (var file in audioFiles)
{
var relative = Path.GetRelativePath(audioRoot, file).Replace('\\', '/');
if (existingSet.Contains(relative)) continue;
var surahNumber = GuessSurahNumber(relative, file);
var ayahNumber = GuessAyahNumber(Path.GetFileNameWithoutExtension(file));
sort++;
db.Episodes.Add(new TafsirEpisode
{
Id = Guid.NewGuid(),
TafsirId = GhatrehChekaniId,
SurahId = surahNumber,
AyahNumber = ayahNumber,
Title = $"{SurahNames[surahNumber - 1].Fa} — {Path.GetFileNameWithoutExtension(file)}",
AudioUrl = relative,
DurationSeconds = 0,
SortOrder = sort,
Summary = null
});
added++;
}
if (added > 0)
await db.SaveChangesAsync(ct);
}
private static int GuessSurahNumber(string relative, string fullPath)
{
var parts = relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var digits = new string(part.TakeWhile(char.IsDigit).ToArray());
if (digits.Length >= 1 && int.TryParse(digits, out var n) && n is >= 1 and <= 114)
return n;
}
var name = Path.GetFileName(fullPath);
var nameDigits = new string(name.TakeWhile(char.IsDigit).ToArray());
if (nameDigits.Length >= 1 && int.TryParse(nameDigits, out var fromName) && fromName is >= 1 and <= 114)
return fromName;
return 1;
}
private static int GuessAyahNumber(string fileName)
{
// patterns: ..._001, -1, .1, ayah01
var matches = System.Text.RegularExpressions.Regex.Matches(fileName, @"(\d+)");
if (matches.Count == 0) return 1;
// prefer last number group as ayah when multiple
if (int.TryParse(matches[^1].Value, out var n) && n > 0)
return n;
return 1;
}
}
@@ -2,9 +2,9 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TafsirQuran.Infrastructure.Persistence;
#nullable disable
@@ -12,8 +12,8 @@ using TafsirQuran.Infrastructure.Persistence;
namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20261009174748_InitialCreate")]
partial class InitialCreate
[Migration("20261009190049_InitialSqlServer")]
partial class InitialSqlServer
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -21,32 +21,32 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
.HasAnnotation("Relational:MaxIdentifierLength", 128);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("TafsirQuran.Domain.Entities.AppUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
.HasColumnType("nvarchar(128)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
@@ -60,20 +60,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<int>("Number")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("SurahId")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<string>("TextAr")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<string>("TextFa")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
@@ -87,25 +87,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
.HasColumnType("nvarchar(500)");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -114,7 +114,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "EpisodeId")
.IsUnique();
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Bookmarks");
});
@@ -123,21 +124,21 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -146,7 +147,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "EpisodeId")
.IsUnique();
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Favorites");
});
@@ -155,24 +157,24 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<double>("PositionSeconds")
.HasColumnType("double precision");
.HasColumnType("float");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -189,22 +191,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
.HasColumnType("int");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("NameAr")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
.HasColumnType("nvarchar(80)");
b.Property<string>("NameFa")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
.HasColumnType("nvarchar(80)");
b.Property<int>("Number")
.HasColumnType("integer");
.HasColumnType("int");
b.HasKey("Id");
@@ -218,28 +220,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
.HasColumnType("nvarchar(120)");
b.Property<string>("CoverUrl")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("boolean");
.HasColumnType("bit");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
.HasColumnType("nvarchar(120)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
@@ -253,34 +255,34 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("AudioUrl")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<int>("AyahNumber")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("DurationSeconds")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<string>("Summary")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<int>("SurahId")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<Guid>("TafsirId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
.HasColumnType("nvarchar(300)");
b.HasKey("Id");
@@ -1,13 +1,12 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
public partial class InitialSqlServer : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
@@ -16,11 +15,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Surahs",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Number = table.Column<int>(type: "integer", nullable: false),
NameAr = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
NameFa = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false)
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Number = table.Column<int>(type: "int", nullable: false),
NameAr = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
NameFa = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false)
},
constraints: table =>
{
@@ -31,12 +30,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Tafsirs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Author = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Slug = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
CoverUrl = table.Column<string>(type: "text", nullable: true),
IsPublished = table.Column<bool>(type: "boolean", nullable: false)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Author = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
Slug = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
CoverUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsPublished = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
@@ -47,11 +46,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
DisplayName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
@@ -62,11 +61,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Ayahs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
SurahId = table.Column<int>(type: "integer", nullable: false),
Number = table.Column<int>(type: "integer", nullable: false),
TextAr = table.Column<string>(type: "text", nullable: false),
TextFa = table.Column<string>(type: "text", nullable: true)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SurahId = table.Column<int>(type: "int", nullable: false),
Number = table.Column<int>(type: "int", nullable: false),
TextAr = table.Column<string>(type: "nvarchar(max)", nullable: false),
TextFa = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
@@ -83,15 +82,15 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Episodes",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
TafsirId = table.Column<Guid>(type: "uuid", nullable: false),
SurahId = table.Column<int>(type: "integer", nullable: false),
AyahNumber = table.Column<int>(type: "integer", nullable: false),
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
AudioUrl = table.Column<string>(type: "text", nullable: false),
DurationSeconds = table.Column<int>(type: "integer", nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
Summary = table.Column<string>(type: "text", nullable: true)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TafsirId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SurahId = table.Column<int>(type: "int", nullable: false),
AyahNumber = table.Column<int>(type: "int", nullable: false),
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
AudioUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
DurationSeconds = table.Column<int>(type: "int", nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false),
Summary = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
@@ -114,12 +113,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Bookmarks",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: true),
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
Note = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
@@ -141,11 +140,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "Favorites",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: true),
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
@@ -167,12 +166,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "ListeningProgresses",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: true),
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
PositionSeconds = table.Column<double>(type: "double precision", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PositionSeconds = table.Column<double>(type: "float", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
@@ -200,7 +199,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "IX_Bookmarks_DeviceId_UserId_EpisodeId",
table: "Bookmarks",
columns: new[] { "DeviceId", "UserId", "EpisodeId" },
unique: true);
unique: true,
filter: "[UserId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_Bookmarks_EpisodeId",
@@ -226,7 +226,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
name: "IX_Favorites_DeviceId_UserId_EpisodeId",
table: "Favorites",
columns: new[] { "DeviceId", "UserId", "EpisodeId" },
unique: true);
unique: true,
filter: "[UserId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_Favorites_EpisodeId",
@@ -2,8 +2,8 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TafsirQuran.Infrastructure.Persistence;
#nullable disable
@@ -18,32 +18,32 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
.HasAnnotation("Relational:MaxIdentifierLength", 128);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("TafsirQuran.Domain.Entities.AppUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
.HasColumnType("nvarchar(128)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
@@ -57,20 +57,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<int>("Number")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("SurahId")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<string>("TextAr")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<string>("TextFa")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
@@ -84,25 +84,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
.HasColumnType("nvarchar(500)");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -111,7 +111,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "EpisodeId")
.IsUnique();
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Bookmarks");
});
@@ -120,21 +121,21 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -143,7 +144,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "EpisodeId")
.IsUnique();
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Favorites");
});
@@ -152,24 +154,24 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("nvarchar(64)");
b.Property<Guid>("EpisodeId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<double>("PositionSeconds")
.HasColumnType("double precision");
.HasColumnType("float");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("datetimeoffset");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
@@ -186,22 +188,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
.HasColumnType("int");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("NameAr")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
.HasColumnType("nvarchar(80)");
b.Property<string>("NameFa")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
.HasColumnType("nvarchar(80)");
b.Property<int>("Number")
.HasColumnType("integer");
.HasColumnType("int");
b.HasKey("Id");
@@ -215,28 +217,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
.HasColumnType("nvarchar(120)");
b.Property<string>("CoverUrl")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("boolean");
.HasColumnType("bit");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
.HasColumnType("nvarchar(120)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
@@ -250,34 +252,34 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("AudioUrl")
.IsRequired()
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<int>("AyahNumber")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("DurationSeconds")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<string>("Summary")
.HasColumnType("text");
.HasColumnType("nvarchar(max)");
b.Property<int>("SurahId")
.HasColumnType("integer");
.HasColumnType("int");
b.Property<Guid>("TafsirId")
.HasColumnType("uuid");
.HasColumnType("uniqueidentifier");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
.HasColumnType("nvarchar(300)");
b.HasKey("Id");
@@ -9,13 +9,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
+14
View File
@@ -0,0 +1,14 @@
@using TafsirQuran.UI
@using TafsirQuran.UI.Layout
<Router AppAssembly="typeof(Routes).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(AppLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(AppLayout)">
<p class="muted">صفحه پیدا نشد.</p>
</LayoutView>
</NotFound>
</Router>
@@ -1,9 +0,0 @@
@inherits LayoutComponentBase
@Body
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -1,20 +0,0 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -1,36 +0,0 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
+7 -24
View File
@@ -1,30 +1,13 @@
using TafsirQuran.UI;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TafsirQuran.UI.Services;
using TafsirQuran.Web.Components;
using TafsirQuran.Web;
var builder = WebApplication.CreateBuilder(args);
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
var apiBase = builder.Configuration["ApiBaseAddress"] ?? "http://localhost:5080/";
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddTafsirUi(apiBase);
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
// HTTP NodePort / reverse-proxy; do not force HTTPS redirect in cluster.
app.UseAntiforgery();
app.UseStaticFiles();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(TafsirQuran.UI.Routes).Assembly);
app.Run();
await builder.Build().RunAsync();
@@ -1,23 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5299",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7169;http://localhost:5299",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5299",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+11 -1
View File
@@ -1,14 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TafsirQuran.UI\TafsirQuran.UI.csproj" />
<ProjectReference Include="..\TafsirQuran.Shared\TafsirQuran.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
</Project>
@@ -1,12 +1,13 @@
@using System.Net.Http
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using TafsirQuran.UI
@using TafsirQuran.UI.Layout
@using TafsirQuran.UI.Pages
@using TafsirQuran.UI.Components
@using TafsirQuran.Web
@using TafsirQuran.Web.Components
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"ApiBaseAddress": "http://localhost:5080/",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,3 @@
{
"ApiBaseAddress": "http://localhost:5080/"
}
@@ -0,0 +1,3 @@
{
"ApiBaseAddress": "http://185.252.31.44:30821/"
}
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
@@ -6,19 +6,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1f6b4a" />
<meta name="description" content="تفسیر قرآن کریم — صوت و متن" />
<title>تفسیر قرآن</title>
<base href="/" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="icons/icon-192.png" />
<link rel="stylesheet" href="_content/TafsirQuran.UI/css/app.css" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<HeadOutlet @rendermode="InteractiveServer" />
<link rel="stylesheet" href="app.css" />
<link href="TafsirQuran.Web.styles.css" rel="stylesheet" />
</head>
<body>
<TafsirQuran.UI.Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<div id="app">
<div class="muted" style="padding:2rem;text-align:center">در حال بارگذاری...</div>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/TafsirQuran.UI/js/app.js"></script>
<script src="service-worker-register.js"></script>
</body>
</html>
@@ -1,5 +0,0 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').catch(() => {});
});
}
+2 -25
View File
@@ -1,25 +1,2 @@
const CACHE = 'tafsirquran-static-v1';
const ASSETS = [
'/',
'/manifest.webmanifest',
'/_content/TafsirQuran.UI/css/app.css',
'/_content/TafsirQuran.UI/js/app.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
event.respondWith(
caches.match(req).then((cached) => cached || fetch(req).catch(() => caches.match('/')))
);
});
// Development placeholder — published SW is service-worker.published.js
self.addEventListener('fetch', () => { });
@@ -0,0 +1,31 @@
// Cache static WASM assets only — never HTML navigations.
const CACHE = 'tafsirquran-wasm-v1';
self.addEventListener('install', event => event.waitUntil(self.skipWaiting()));
self.addEventListener('activate', event => event.waitUntil(
caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
.then(() => self.clients.claim())
));
self.addEventListener('fetch', event => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (req.mode === 'navigate' || (req.headers.get('accept') || '').includes('text/html')) {
event.respondWith(fetch(req));
return;
}
if (url.pathname.startsWith('/api/') || url.origin.includes(':30821')) {
event.respondWith(fetch(req));
return;
}
event.respondWith(
caches.open(CACHE).then(async cache => {
const cached = await cache.match(req);
if (cached) return cached;
const res = await fetch(req);
if (res.ok) cache.put(req, res.clone());
return res;
})
);
});