Compare commits
No commits in common. "feature/pwa" and "main" have entirely different histories.
feature/pw
...
main
@ -1,30 +0,0 @@
|
|||||||
**/.classpath
|
|
||||||
**/.dockerignore
|
|
||||||
**/.env
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/.project
|
|
||||||
**/.settings
|
|
||||||
**/.toolstarget
|
|
||||||
**/.vs
|
|
||||||
**/.vscode
|
|
||||||
**/*.*proj.user
|
|
||||||
**/*.dbmdl
|
|
||||||
**/*.jfm
|
|
||||||
**/azds.yaml
|
|
||||||
**/bin
|
|
||||||
**/charts
|
|
||||||
**/docker-compose*
|
|
||||||
**/Dockerfile*
|
|
||||||
**/node_modules
|
|
||||||
**/npm-debug.log
|
|
||||||
**/obj
|
|
||||||
**/secrets.dev.yaml
|
|
||||||
**/values.dev.yaml
|
|
||||||
LICENSE
|
|
||||||
README.md
|
|
||||||
!**/.gitignore
|
|
||||||
!.git/HEAD
|
|
||||||
!.git/config
|
|
||||||
!.git/packed-refs
|
|
||||||
!.git/refs/heads/**
|
|
||||||
428
.gitignore
vendored
@ -1,428 +0,0 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
|
||||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.rsuser
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
*.env
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Mono auto generated files
|
|
||||||
mono_crash.*
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
|
|
||||||
[Dd]ebug/x64/
|
|
||||||
[Dd]ebugPublic/x64/
|
|
||||||
[Rr]elease/x64/
|
|
||||||
[Rr]eleases/x64/
|
|
||||||
bin/x64/
|
|
||||||
obj/x64/
|
|
||||||
|
|
||||||
[Dd]ebug/x86/
|
|
||||||
[Dd]ebugPublic/x86/
|
|
||||||
[Rr]elease/x86/
|
|
||||||
[Rr]eleases/x86/
|
|
||||||
bin/x86/
|
|
||||||
obj/x86/
|
|
||||||
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
|
||||||
bld/
|
|
||||||
[Oo]bj/
|
|
||||||
[Oo]ut/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Build results on 'Bin' directories
|
|
||||||
**/[Bb]in/*
|
|
||||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
|
||||||
# (https://github.com/github/gitignore/pull/3736)
|
|
||||||
#!**/[Bb]in/*.refresh
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
|
||||||
.vs/
|
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
*.trx
|
|
||||||
|
|
||||||
# NUnit
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Approval Tests result files
|
|
||||||
*.received.*
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# ASP.NET Scaffolding
|
|
||||||
ScaffoldingReadMe.txt
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.idb
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
|
||||||
!Directory.Build.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
|
||||||
*.tlog
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
*.VC.db
|
|
||||||
*.VC.VC.opendb
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool
|
|
||||||
.axoCover/*
|
|
||||||
!.axoCover/settings.json
|
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
|
||||||
# in these scripts will be unencrypted
|
|
||||||
PublishScripts/
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/[Pp]ackages/repositories.config
|
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
Package.StoreAssociation.xml
|
|
||||||
_pkginfo.txt
|
|
||||||
*.appx
|
|
||||||
*.appxbundle
|
|
||||||
*.appxupload
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
*.ndf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
*.rptproj.rsuser
|
|
||||||
*- [Bb]ackup.rdl
|
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
|
||||||
*.vbw
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
|
||||||
*.dsw
|
|
||||||
*.dsp
|
|
||||||
|
|
||||||
# Visual Studio 6 technical files
|
|
||||||
*.ncb
|
|
||||||
*.aps
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
**/.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
**/.fake/
|
|
||||||
|
|
||||||
# CodeRush personal settings
|
|
||||||
**/.cr/personal
|
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
**/__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
|
||||||
#tools/**
|
|
||||||
#!tools/packages.config
|
|
||||||
|
|
||||||
# Tabs Studio
|
|
||||||
*.tss
|
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
|
||||||
*.jmconfig
|
|
||||||
|
|
||||||
# BizTalk build output
|
|
||||||
*.btp.cs
|
|
||||||
*.btm.cs
|
|
||||||
*.odx.cs
|
|
||||||
*.xsd.cs
|
|
||||||
|
|
||||||
# OpenCover UI analysis results
|
|
||||||
OpenCover/
|
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
|
||||||
ASALocalRun/
|
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
|
||||||
*.binlog
|
|
||||||
MSBuild_Logs/
|
|
||||||
|
|
||||||
# AWS SAM Build and Temporary Artifacts folder
|
|
||||||
.aws-sam
|
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file
|
|
||||||
*.nvuser
|
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
|
||||||
**/.mfractor/
|
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
**/.localhistory/
|
|
||||||
|
|
||||||
# Visual Studio History (VSHistory) files
|
|
||||||
.vshistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
|
||||||
MigrationBackup/
|
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
|
||||||
**/.ionide/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
|
|
||||||
# VS Code files for those working on multiple tools
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
|
||||||
.history/
|
|
||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
|
||||||
*.vsix
|
|
||||||
|
|
||||||
# Windows Installer files from build outputs
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Auth
|
|
||||||
{
|
|
||||||
public record ApiUser(Guid Id, string UserName, string Email, string[] Roles);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
using OnlyPrompt.Backend.ApiModels.Validators;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.ApiModels.Auth
|
|
||||||
{
|
|
||||||
public record ApiLoginRequest(string UserNameOrEmail, string Password);
|
|
||||||
public record ApiRegisterRequest([MaxLength(100)] string DisplayName, [MaxLength(100)][NoWhitespace] string? UserName, string Email, string Password);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Category
|
|
||||||
{
|
|
||||||
public record ApiCategory(Guid Id, string Name, string Slug, string? Description);
|
|
||||||
public record ApiMinimalCategory(string Name, string Slug);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Category
|
|
||||||
{
|
|
||||||
public record ApiCreateCategoryRequest(string Name, string? Slug, string? Description);
|
|
||||||
public record ApiUpdateCategoryRequest(string? Name, string? Slug, string? Description);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
|
||||||
{
|
|
||||||
public enum FeedSortType
|
|
||||||
{
|
|
||||||
Date,
|
|
||||||
Rating
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
|
||||||
{
|
|
||||||
public record ApiPrompt(Guid Id, string Title, string Description, string Content, DateTime TimeStamp, Guid CreatorId, string CreatorName, int? TierLevel, string? TierName, double? AverageRating);
|
|
||||||
public record ApiMinimalPrompt(Guid Id, string Title, DateTime TimeStamp, Guid CreatorId, string CreatorName, int? TierLevel, string? TierName, double? AverageRating, bool CanAccess);
|
|
||||||
public record ApiReview(Guid CreatorId, string CreatorName, string? Comment, int Rating);
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
|
||||||
{
|
|
||||||
public record ApiCreatePromptRequest(string Title, string Description, string Content, Identifier Category, int? SubscriptionTier, string Slug);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Subscription
|
|
||||||
{
|
|
||||||
public record ApiSubscriptionTier(Guid Id, string Name, int Level, decimal MonthlyPrice, string? Description);
|
|
||||||
public record ApiSubscription(Guid SubscribedToId, string SubscribedToName, ApiSubscriptionTier? CurrentTier);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.Subscription
|
|
||||||
{
|
|
||||||
public record ApiCreateSubscriptionTierRequest(string Name, decimal MonthlyPrice, int Level, string? Description);
|
|
||||||
public record ApiUpdateSubscriptionTierRequest(string? Name, decimal? MonthlyPrice, int? Level, string? Description);
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
|
||||||
{
|
|
||||||
public record ApiUserProfile(string DisplayName, string Slug, string? Bio, string AvatarUrl, string? Specialities, double AverageRating, int Subscribers);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
using OnlyPrompt.Backend.ApiModels.Validators;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
|
||||||
{
|
|
||||||
public record ApiUpdateProfileRequest([MaxLength(100)] string? DisplayName, [MaxLength(100)][NoWhitespace] string? Slug, string? Bio, string? AvatarUrl, string? Specialities, bool IsPublic);
|
|
||||||
public record ApiCreateReviewRequest(string? Comment, [Range(1, 5)] int Rating);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.ApiModels.Validators
|
|
||||||
{
|
|
||||||
public class NoWhitespaceAttribute : ValidationAttribute
|
|
||||||
{
|
|
||||||
public override bool IsValid(object? value)
|
|
||||||
{
|
|
||||||
if (value is string strValue)
|
|
||||||
{
|
|
||||||
if (strValue.Any(c => char.IsWhiteSpace(c)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // If it's not a string, we consider it valid. Use [NoWhitespace] only on string properties.
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
|
||||||
{
|
|
||||||
if(value is string strValue)
|
|
||||||
{
|
|
||||||
if(strValue.Any(c => char.IsWhiteSpace(c)))
|
|
||||||
return new ValidationResult($"{validationContext.DisplayName} should not contain any whitespace characters.");
|
|
||||||
|
|
||||||
return ValidationResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidationResult.Success; // If it's not a string, we consider it valid. Use [NoWhitespace] only on string properties.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/admin")]
|
|
||||||
[Authorize(Roles = ModelConstants.AdminRole)]
|
|
||||||
public class AdminController : BaseController
|
|
||||||
{
|
|
||||||
public AdminController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<UserModel?> GetNonAdminUserAsync(Guid id, bool isSysAdmin = false)
|
|
||||||
{
|
|
||||||
return _db.Users.FirstOrDefaultAsync(
|
|
||||||
u => u.Id == id
|
|
||||||
&& (isSysAdmin || u.Roles.Contains(ModelConstants.AdminRole) == false)
|
|
||||||
&& u.Roles.Contains(ModelConstants.SysAdminRole) == false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("users/{userId}/disable")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> DisableUserAsync(Guid userId)
|
|
||||||
{
|
|
||||||
var user = await GetNonAdminUserAsync(userId, User.IsInRole(ModelConstants.SysAdminRole));
|
|
||||||
if (user is null)
|
|
||||||
return TypedResults.NotFound("User not found.");
|
|
||||||
|
|
||||||
user.IsLockoutEnabled = true;
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("users/{userId}/enable")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> EnableUserAsync(Guid userId)
|
|
||||||
{
|
|
||||||
var user = await GetNonAdminUserAsync(userId, User.IsInRole(ModelConstants.SysAdminRole));
|
|
||||||
if (user is null)
|
|
||||||
return TypedResults.NotFound("User not found.");
|
|
||||||
|
|
||||||
user.IsLockoutEnabled = false;
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("users/{userId}/roles/{role}")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> UpdateUserRolesAsync(Guid userId, string role)
|
|
||||||
{
|
|
||||||
if (ModelConstants.AllRoles.Contains(role) == false)
|
|
||||||
return TypedResults.NotFound($"No such role '{role}'");
|
|
||||||
|
|
||||||
var user = await GetNonAdminUserAsync(userId, User.IsInRole(ModelConstants.SysAdminRole));
|
|
||||||
if (user is null)
|
|
||||||
return TypedResults.NotFound("User not found.");
|
|
||||||
|
|
||||||
user.Roles.Add(role);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("users/{userId}/roles/{role}")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> RemoveUserRoleAsync(Guid userId, string role)
|
|
||||||
{
|
|
||||||
if (ModelConstants.AllRoles.Contains(role) == false)
|
|
||||||
return TypedResults.NotFound($"No such role '{role}'");
|
|
||||||
|
|
||||||
var user = await GetNonAdminUserAsync(userId, User.IsInRole(ModelConstants.SysAdminRole));
|
|
||||||
if (user is null)
|
|
||||||
return TypedResults.NotFound("User not found.");
|
|
||||||
|
|
||||||
user.Roles.Remove(role);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Auth;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Services.Jwt;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/auth")]
|
|
||||||
public class AuthController : BaseController
|
|
||||||
{
|
|
||||||
private static readonly CookieOptions AuthCookieOptions = new CookieOptions { Secure = true, HttpOnly = true, IsEssential = true };
|
|
||||||
private readonly IPasswordHasher<UserModel> _passwordHasher;
|
|
||||||
private readonly ITokenService _jwtService;
|
|
||||||
private readonly ILogger<AuthController> _logger;
|
|
||||||
|
|
||||||
public AuthController(OnlyPromptContext db, IPasswordHasher<UserModel> passwordHasher, IMapper mapper, ILogger<AuthController> logger, ITokenService jwtService) : base(db, mapper)
|
|
||||||
{
|
|
||||||
_passwordHasher=passwordHasher;
|
|
||||||
_logger=logger;
|
|
||||||
_jwtService=jwtService;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("login")]
|
|
||||||
public async Task<Results<Ok, RedirectHttpResult, BadRequest<string>, NotFound<string>>> LoginAsync([FromBody] ApiLoginRequest request, [FromQuery]string redirect = null)
|
|
||||||
{
|
|
||||||
var user = await FindUserAsync(request.UserNameOrEmail);
|
|
||||||
if (user is null)
|
|
||||||
return TypedResults.NotFound("User not found");
|
|
||||||
|
|
||||||
var verificationResult = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
|
|
||||||
if (verificationResult == PasswordVerificationResult.Failed)
|
|
||||||
return TypedResults.NotFound("User not found"); // Don't reveal that the user exists
|
|
||||||
|
|
||||||
if (user.IsLockoutEnabled)
|
|
||||||
return TypedResults.BadRequest("User is locked out"); // Don't reveal that the user exists
|
|
||||||
|
|
||||||
var token = _jwtService.BuildToken(user, out var validUntil);
|
|
||||||
this.Response.Cookies.Append("jwt", token, AuthCookieOptions.Copy(c => c.Expires = validUntil));
|
|
||||||
if (string.IsNullOrEmpty(redirect) == false)
|
|
||||||
return TypedResults.Redirect(redirect, false);
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("register")]
|
|
||||||
public async Task<Results<RedirectHttpResult, ValidationProblem, Ok<ApiUser>>> RegisterAsync([FromBody] ApiRegisterRequest request, [FromQuery] string redirect = null)
|
|
||||||
{
|
|
||||||
var existingUser = await FindUserAsync(request.UserName, request.Email);
|
|
||||||
if (existingUser is not null)
|
|
||||||
{
|
|
||||||
var errors = new Dictionary<string, string[]>();
|
|
||||||
|
|
||||||
if (existingUser.UserName == request.UserName)
|
|
||||||
errors.Add(nameof(request.UserName), ["Username is already taken"]);
|
|
||||||
|
|
||||||
if (existingUser.Email == request.Email)
|
|
||||||
errors.Add(nameof(request.Email), ["Email is already registered"]);
|
|
||||||
|
|
||||||
return TypedResults.ValidationProblem(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = Guid.CreateVersion7();
|
|
||||||
var slug = await SlugHelper.GenerateUniqueSlugAsync(request.UserName, s => _db.UserProfiles.AnyAsync(up => up.Slug == s), 32);
|
|
||||||
var avatarUrl = $"https://api.dicebear.com/9.x/bottts/svg?seed={id}";
|
|
||||||
var newUser = new UserModel
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Profile = new UserProfileModel
|
|
||||||
{
|
|
||||||
AvatarUrl = avatarUrl,
|
|
||||||
DisplayName = request.DisplayName,
|
|
||||||
Slug = slug,
|
|
||||||
},
|
|
||||||
Roles = [ModelConstants.UserRole],
|
|
||||||
PasswordHash = null,
|
|
||||||
UserName = request.UserName ?? request.Email,
|
|
||||||
Email = request.Email,
|
|
||||||
IsLockoutEnabled = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password);
|
|
||||||
_db.Users.Add(newUser);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
if(string.IsNullOrEmpty(redirect) == false)
|
|
||||||
return TypedResults.Redirect(redirect, false);
|
|
||||||
|
|
||||||
return TypedResults.Ok(_mapper.Map<ApiUser>(newUser));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
|
||||||
public RedirectHttpResult Logout()
|
|
||||||
{
|
|
||||||
this.Response.Cookies.Delete("jwt", AuthCookieOptions);
|
|
||||||
return TypedResults.Redirect("login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
public abstract class BaseController : Controller
|
|
||||||
{
|
|
||||||
protected OnlyPromptContext _db;
|
|
||||||
protected IMapper _mapper;
|
|
||||||
|
|
||||||
public BaseController(OnlyPromptContext db, IMapper mapper)
|
|
||||||
{
|
|
||||||
_db=db;
|
|
||||||
_mapper=mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<UserModel?> FindUserAsync(Guid id) => _db.Users.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
public Task<UserModel?> FindUserAsync(string userName, string email) => _db.Users.FirstOrDefaultAsync(x => x.Email == email || x.UserName == userName);
|
|
||||||
public Task<UserModel?> FindUserAsync(string emailOrUsername) => _db.Users.FirstOrDefaultAsync(x => x.Email == emailOrUsername || x.UserName == emailOrUsername);
|
|
||||||
|
|
||||||
public async Task<UserProfileModel?> GetUserProfileAsync()
|
|
||||||
{
|
|
||||||
var id = User.GetUserId();
|
|
||||||
if (id.HasValue == false)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var profile = await _db.UserProfiles.FirstOrDefaultAsync(x => x.Id == id.Value);
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserModel?> GetUserAsync()
|
|
||||||
{
|
|
||||||
var id = User.GetUserId();
|
|
||||||
if (id.HasValue == false)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var user = await _db.Users.FindAsync(id.Value);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Category;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/categories")]
|
|
||||||
[Authorize(Roles = ModelConstants.AdminRole, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
|
||||||
public class CategoryController : BaseController
|
|
||||||
{
|
|
||||||
private static ValidationProblem SlugExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
|
||||||
{
|
|
||||||
{ nameof(CategoryModel.Slug), new[] { "Slug already exists." } }
|
|
||||||
});
|
|
||||||
|
|
||||||
public CategoryController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("minimal")]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public async Task<ApiMinimalCategory[]> GetMinimalCategoriesAsync()
|
|
||||||
{
|
|
||||||
var categories = await _db.Categories
|
|
||||||
.ProjectTo<ApiMinimalCategory>(_mapper.ConfigurationProvider)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public async Task<ApiCategory[]> GetCategoriesAsync([Range(0, double.MaxValue)][FromQuery] int offset = 0, [Range(1, 100)][FromQuery] int limit = 20)
|
|
||||||
{
|
|
||||||
var categories = await _db.Categories
|
|
||||||
.OrderBy(c => c.Id)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.ProjectTo<ApiCategory>(_mapper.ConfigurationProvider)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<Results<Ok<ApiCategory>, ValidationProblem>> CreateCategoryAsync([FromBody] ApiCreateCategoryRequest request)
|
|
||||||
{
|
|
||||||
var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug);
|
|
||||||
if (exists)
|
|
||||||
return SlugExistsProblem;
|
|
||||||
|
|
||||||
var model = _mapper.Map<CategoryModel>(request);
|
|
||||||
if (string.IsNullOrWhiteSpace(model.Slug))
|
|
||||||
model.Slug = await SlugHelper.GenerateUniqueSlugAsync(request.Name, slug => _db.Categories.AnyAsync(c => c.Slug == slug), ModelConstants.MaxSlugLength);
|
|
||||||
|
|
||||||
_db.Categories.Add(model);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok(_mapper.Map<ApiCategory>(model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task<Results<Ok<ApiCategory>, NotFound<string>, ValidationProblem>> UpdateCategoryAsync(Identifier id, [FromBody] ApiUpdateCategoryRequest request)
|
|
||||||
{
|
|
||||||
var category = await _db.Categories.FindByIdentifierAsync(id);
|
|
||||||
if (category is null)
|
|
||||||
return TypedResults.NotFound("Category not found");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Name) == false)
|
|
||||||
category.Name = request.Name;
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(request.Description) == false)
|
|
||||||
category.Description = request.Description;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Slug) == false && request.Slug != category.Slug)
|
|
||||||
{
|
|
||||||
var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug && c.Id != category.Id);
|
|
||||||
if (exists)
|
|
||||||
return SlugExistsProblem;
|
|
||||||
|
|
||||||
category.Slug = request.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok(_mapper.Map<ApiCategory>(category));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<Results<NoContent, BadRequest<string>, NotFound<string>>> DeleteCategoryAsync(Identifier id, [FromQuery] Identifier? replaceWith = null)
|
|
||||||
{
|
|
||||||
var hasPrompts = await _db.Prompts.AnyAsync(p => p.CategoryId == id.Id);
|
|
||||||
if (hasPrompts)
|
|
||||||
{
|
|
||||||
if (replaceWith.HasValue == false)
|
|
||||||
return TypedResults.BadRequest("Category has associated prompts. Provide a replacement category to reassign them to.");
|
|
||||||
|
|
||||||
var replacement = await _db.Categories.FindByIdentifierAsync(replaceWith.Value);
|
|
||||||
if(replacement is null)
|
|
||||||
return TypedResults.NotFound("Replacement category not found.");
|
|
||||||
|
|
||||||
await _db.Prompts.Where(p => p.CategoryId == id.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(p => p.CategoryId, replacement.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = await _db.Categories.OfIdentifer(id)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
return TypedResults.NotFound("Category not found");
|
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Prompt;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/feed")]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public class FeedController : BaseController
|
|
||||||
{
|
|
||||||
public FeedController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ApiMinimalPrompt[]> GetFeedAsync(
|
|
||||||
[Range(0, double.MaxValue)][FromQuery]int offset = 0,
|
|
||||||
[Range(1, 100)][FromQuery]int limit = 20,
|
|
||||||
[FromQuery]FeedSortType sortBy = FeedSortType.Date,
|
|
||||||
[FromQuery]bool ascending = false,
|
|
||||||
[FromQuery]Identifier? category = null,
|
|
||||||
[FromQuery]DateTime? fromDate = null,
|
|
||||||
[FromQuery]DateTime? toDate = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var query = _db.Prompts
|
|
||||||
.Where(
|
|
||||||
x => x.Creator.Subscribers.Any(s => s.SubscriberId == userId)
|
|
||||||
&& x.CreatorId != userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (category.HasValue)
|
|
||||||
query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug);
|
|
||||||
|
|
||||||
if (fromDate.HasValue)
|
|
||||||
query = query.Where(x => x.UpdatedAt >= fromDate.Value);
|
|
||||||
|
|
||||||
if (toDate.HasValue)
|
|
||||||
query = query.Where(x => x.UpdatedAt <= toDate.Value);
|
|
||||||
|
|
||||||
query = sortBy switch {
|
|
||||||
FeedSortType.Date => query.OrderBy(x => x.UpdatedAt, ascending),
|
|
||||||
FeedSortType.Rating => query.OrderBy(x => x.Reviews.Average(r => (double?)r.Rating) ?? 2.5, ascending),
|
|
||||||
_ => query
|
|
||||||
};
|
|
||||||
|
|
||||||
var prompts = await query
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(x => new ApiMinimalPrompt(
|
|
||||||
x.Id,
|
|
||||||
x.Title,
|
|
||||||
x.UpdatedAt,
|
|
||||||
x.CreatorId,
|
|
||||||
x.Creator.Profile.DisplayName,
|
|
||||||
x.SubscriptionTier.Level,
|
|
||||||
x.SubscriptionTier.Name,
|
|
||||||
x.Reviews.Average(r => (double?)r.Rating),
|
|
||||||
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
|
||||||
)).ToArrayAsync();
|
|
||||||
|
|
||||||
return prompts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.UserProfile;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/profiles")]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public class ProfileController : BaseController
|
|
||||||
{
|
|
||||||
private static ValidationProblem SlugExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
|
||||||
{
|
|
||||||
{ nameof(UserProfileModel.Slug), new[] { "Slug already exists." } }
|
|
||||||
});
|
|
||||||
|
|
||||||
public ProfileController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task<Results<NotFound<string>, Ok<ApiUserProfile>>> GetProfileAsync(Identifier id)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var profile = await _db.UserProfiles.OfIdentifer(id)
|
|
||||||
.Where(up => up.IsPublic || up.Id == userId)
|
|
||||||
.ProjectTo<ApiUserProfile>(_mapper.ConfigurationProvider)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (profile is null)
|
|
||||||
return TypedResults.NotFound("Profile not found or is private.");
|
|
||||||
|
|
||||||
return TypedResults.Ok(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
public async Task<Results<ValidationProblem, NotFound<string>, Ok<ApiUserProfile>>> UpdateProfileAsync([FromBody] ApiUpdateProfileRequest request)
|
|
||||||
{
|
|
||||||
var self = await GetUserProfileAsync();
|
|
||||||
if (self is null)
|
|
||||||
return TypedResults.NotFound("Profile not found.");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.Slug) == false)
|
|
||||||
{
|
|
||||||
if (await _db.UserProfiles.AnyAsync(up => up.Slug == request.Slug && up.Id != self.Id))
|
|
||||||
return SlugExistsProblem;
|
|
||||||
|
|
||||||
self.Slug = request.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(request.AvatarUrl) == false)
|
|
||||||
self.AvatarUrl = request.AvatarUrl;
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(request.Bio) == false)
|
|
||||||
self.Bio = request.Bio;
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(request.Specialities) == false)
|
|
||||||
self.Specialities = request.Specialities;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.DisplayName) == false)
|
|
||||||
self.DisplayName = request.DisplayName;
|
|
||||||
|
|
||||||
self.IsPublic = request.IsPublic;
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
var result = _mapper.Map<ApiUserProfile>(self);
|
|
||||||
return TypedResults.Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Prompt;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.UserProfile;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/prompts")]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public class PromptController : BaseController
|
|
||||||
{
|
|
||||||
public PromptController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private IQueryable<PromptModel> GetAccessiblePrompts(Guid userId)
|
|
||||||
{
|
|
||||||
return _db.Prompts.Where(
|
|
||||||
p => p.SubscriptionTier == null
|
|
||||||
|| p.Creator.Subscribers.Any(
|
|
||||||
sub => sub.SubscriberId == userId
|
|
||||||
&& p.SubscriptionTier!.Level <= sub.SubscriptionTier!.Level
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> GetPromptAsync(Identifier id)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var prompt = await GetAccessiblePrompts(userId.Value)
|
|
||||||
.OfIdentifer(id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (prompt is null)
|
|
||||||
return TypedResults.NotFound("Prompt not found or no permission");
|
|
||||||
|
|
||||||
var apiPrompt = _mapper.Map<ApiPrompt>(prompt);
|
|
||||||
return TypedResults.Ok(apiPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<Results<NoContent, NotFound<string>>> DeletePromptAsync(Identifier id)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var isAdmin = User.IsInRole(ModelConstants.AdminRole);
|
|
||||||
var count = await _db.Prompts
|
|
||||||
.OfIdentifer(id)
|
|
||||||
.Where(p => p.CreatorId == userId || isAdmin)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
return TypedResults.NotFound("Prompt not found or no permission");
|
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> CreatePromptAsync([FromBody] ApiCreatePromptRequest request)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
|
|
||||||
var category = await _db.Categories.FindByIdentifierAsync(request.Category);
|
|
||||||
if (category is null)
|
|
||||||
return TypedResults.NotFound("Category not found");
|
|
||||||
|
|
||||||
SubscriptionTierModel? subscriptionTier = null;
|
|
||||||
if (request.SubscriptionTier.HasValue)
|
|
||||||
{
|
|
||||||
subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync(
|
|
||||||
t => t.Level == request.SubscriptionTier.Value
|
|
||||||
&& t.UserId == userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (subscriptionTier is null)
|
|
||||||
return TypedResults.NotFound("Subscription tier not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
var slug = request.Slug;
|
|
||||||
if (string.IsNullOrEmpty(slug))
|
|
||||||
slug = await SlugHelper.GenerateUniqueSlugAsync(request.Title, slug => _db.Prompts.AnyAsync(p => p.Slug == slug), ModelConstants.MaxSlugLength);
|
|
||||||
|
|
||||||
var prompt = new PromptModel
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Title = request.Title,
|
|
||||||
Description = request.Description,
|
|
||||||
Prompt = request.Content,
|
|
||||||
CreatorId = userId.Value,
|
|
||||||
SubscriptionTier = subscriptionTier,
|
|
||||||
Category = category,
|
|
||||||
Slug = slug
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Prompts.Add(prompt);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
var apiPrompt = _mapper.Map<ApiPrompt>(prompt);
|
|
||||||
return TypedResults.Ok(apiPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/reviews")]
|
|
||||||
public async Task<Ok<ApiReview[]>> GetReviewsAsync(Identifier id, [FromQuery] int offset = 0, [Range(1, 200)][FromQuery] int limit = 20)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var accessiblePrompts = GetAccessiblePrompts(userId!.Value);
|
|
||||||
var reviews = await accessiblePrompts.Select(x => x.Reviews)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.ProjectTo<ApiReview>(_mapper.ConfigurationProvider)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return TypedResults.Ok(reviews);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}/reviews")]
|
|
||||||
public async Task<Results<Ok<ApiReview>, BadRequest<string>, NotFound<string>>> AddReviewAsync(Identifier id, [FromBody] ApiCreateReviewRequest request)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var prompt = await GetAccessiblePrompts(userId!.Value)
|
|
||||||
.OfIdentifer(id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (prompt is null)
|
|
||||||
return TypedResults.NotFound("Prompt not found or no permission");
|
|
||||||
|
|
||||||
if(prompt.CreatorId == userId)
|
|
||||||
return TypedResults.BadRequest("Cannot review your own prompt");
|
|
||||||
|
|
||||||
var review = await _db.Reviews.FirstOrDefaultAsync(
|
|
||||||
r => r.PromptId == prompt.Id
|
|
||||||
&& r.ReviewerId == userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (review is null)
|
|
||||||
{
|
|
||||||
review = new ReviewModel
|
|
||||||
{
|
|
||||||
PromptId = prompt.Id,
|
|
||||||
ReviewerId = userId.Value,
|
|
||||||
Comment = request.Comment,
|
|
||||||
Rating = request.Rating
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Reviews.Add(review);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
review.Comment = request.Comment;
|
|
||||||
review.Rating = request.Rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
var apiReview = _mapper.Map<ApiReview>(review);
|
|
||||||
return TypedResults.Ok(apiReview);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{promptId}/reviews/{reviewerId}")]
|
|
||||||
public async Task<Results<NoContent, NotFound<string>>> DeleteReviewAsync(Identifier promptId, Guid reviewerId)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var isAdmin = User.IsInRole(ModelConstants.AdminRole);
|
|
||||||
var count = await _db.Reviews
|
|
||||||
.Where(
|
|
||||||
r => (promptId.Id.HasValue ? r.PromptId == promptId.Id : r.Prompt.Slug == promptId.Slug)
|
|
||||||
&& (r.ReviewerId == reviewerId || isAdmin)
|
|
||||||
)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
return TypedResults.NotFound("Review not found or no permission");
|
|
||||||
return TypedResults.NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Subscription;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Formats.Asn1;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/v1/subscriptions")]
|
|
||||||
[Authorize(Roles = ModelConstants.UserRole)]
|
|
||||||
public class SubscriptionController : BaseController
|
|
||||||
{
|
|
||||||
private static ValidationProblem TierLevelExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
|
||||||
{
|
|
||||||
{ nameof(SubscriptionTierModel.Level), new[] { "Tier with this level already exists." } }
|
|
||||||
});
|
|
||||||
public SubscriptionController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{userId}/{level}")]
|
|
||||||
public async Task<Results<Ok, BadRequest<string>, NotFound<string>>> SubscribeAsync(Identifier subscribeToId, int? level = null)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var subscribeTo = await _db.Users.Include(x => x.SubscriptionTiers.Where(st => st.Level == level))
|
|
||||||
.FirstOrDefaultAsync(
|
|
||||||
user => subscribeToId.Id.HasValue ? user.Id == subscribeToId.Id.Value : user.Profile.Slug == subscribeToId.Slug
|
|
||||||
);
|
|
||||||
|
|
||||||
if (subscribeTo is null)
|
|
||||||
return TypedResults.NotFound($"No user found with identifier {subscribeToId}");
|
|
||||||
|
|
||||||
if (subscribeTo.Id == userId)
|
|
||||||
return TypedResults.BadRequest("Cannot subscribe to yourself");
|
|
||||||
|
|
||||||
SubscriptionTierModel? tier = subscribeTo.SubscriptionTiers.FirstOrDefault();
|
|
||||||
if (level.HasValue && tier is null)
|
|
||||||
return TypedResults.NotFound($"No subscription tier found for user {subscribeToId} with level {level.Value}");
|
|
||||||
|
|
||||||
var existingSubscription = await _db.Subscriptions.FirstOrDefaultAsync(
|
|
||||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
|
||||||
&& sub.SubscriberId == userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingSubscription is null)
|
|
||||||
{
|
|
||||||
existingSubscription = new SubscriptionModel
|
|
||||||
{
|
|
||||||
SubscribedTo = subscribeTo,
|
|
||||||
SubscriberId = userId.Value,
|
|
||||||
SubscriptionTier = tier
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Subscriptions.Add(existingSubscription);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingSubscription.SubscriptionTier = tier;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ApiSubscription[]> GetSubscriptionsAsync([Range(0, double.MaxValue)]int offset = 0, [Range(1, 100)]int limit = 20)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var subscriptions = await _db.Subscriptions
|
|
||||||
.Where(x => x.SubscriberId == userId)
|
|
||||||
.OrderBy(x => x.SubscribedToId)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.ProjectTo<ApiSubscription>(_mapper.ConfigurationProvider)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return subscriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{userId}")]
|
|
||||||
public async Task<ApiSubscription?> GetCurrentSubscriptionAsync(Identifier subscribeToId)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var subscription = await _db.Subscriptions
|
|
||||||
.Where(
|
|
||||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
|
||||||
&& sub.SubscriberId == userId
|
|
||||||
)
|
|
||||||
.ProjectTo<ApiSubscription>(_mapper.ConfigurationProvider)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{userId}")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> UnsubscribeAsync(Identifier subscribeToId)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var count = await _db.Subscriptions
|
|
||||||
.Where(
|
|
||||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
|
||||||
&& sub.SubscriberId == userId
|
|
||||||
)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
return TypedResults.NotFound($"No subscription found for user {subscribeToId}");
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("tiers")]
|
|
||||||
public async Task<Results<Ok<ApiSubscriptionTier>, ValidationProblem>> CreateOrUpdateSubscriptionTierAsync([FromBody] ApiCreateSubscriptionTierRequest tier)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var levelExists = await _db.SubscriptionTiers.AnyAsync(t => t.UserId == userId && t.Level == tier.Level);
|
|
||||||
if (levelExists)
|
|
||||||
return TierLevelExistsProblem;
|
|
||||||
|
|
||||||
var model = _mapper.Map<SubscriptionTierModel>(tier);
|
|
||||||
model.UserId = userId!.Value;
|
|
||||||
_db.SubscriptionTiers.Add(model);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok(_mapper.Map<ApiSubscriptionTier>(model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("tiers/{id}")]
|
|
||||||
public async Task<Results<Ok<ApiSubscriptionTier>, NotFound<string>, ValidationProblem>> UpdateSubscriptionTierAsync(Guid id, [FromBody] ApiUpdateSubscriptionTierRequest tier)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var existingTier = await _db.SubscriptionTiers.FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);
|
|
||||||
if (existingTier is null)
|
|
||||||
return TypedResults.NotFound($"No subscription tier found with id {id}");
|
|
||||||
|
|
||||||
if (existingTier.Level != tier.Level)
|
|
||||||
{
|
|
||||||
var levelExists = await _db.SubscriptionTiers.AnyAsync(t => t.UserId == userId && t.Level == tier.Level);
|
|
||||||
if (levelExists)
|
|
||||||
return TierLevelExistsProblem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(tier.Name) == false)
|
|
||||||
existingTier.Name = tier.Name;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(tier.Description) == false)
|
|
||||||
existingTier.Description = tier.Description;
|
|
||||||
|
|
||||||
if (tier.Level.HasValue)
|
|
||||||
existingTier.Level = tier.Level.Value;
|
|
||||||
|
|
||||||
if (tier.MonthlyPrice.HasValue)
|
|
||||||
existingTier.MonthlyPrice = tier.MonthlyPrice.Value;
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return TypedResults.Ok(_mapper.Map<ApiSubscriptionTier>(existingTier));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("tiers/{id}")]
|
|
||||||
public async Task<Results<Ok, NotFound<string>>> DeleteSubscriptionTierAsync(Guid id)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
var count = await _db.SubscriptionTiers
|
|
||||||
.Where(t => t.Id == id && t.UserId == userId)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
return TypedResults.NotFound($"No subscription tier found with id {id}");
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Core
|
|
||||||
{
|
|
||||||
public class EntityBase : IEntity, ITrackableEntity
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.Database.Core
|
|
||||||
{
|
|
||||||
public interface IEntity
|
|
||||||
{
|
|
||||||
public Guid Id { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.Database.Core
|
|
||||||
{
|
|
||||||
public interface IHasSlug
|
|
||||||
{
|
|
||||||
public string Slug { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
namespace OnlyPrompt.Backend.Database.Core
|
|
||||||
{
|
|
||||||
public interface ITrackableEntity
|
|
||||||
{
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime UpdatedAt { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
using System.Collections.Frozen;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database
|
|
||||||
{
|
|
||||||
public static class ModelConstants
|
|
||||||
{
|
|
||||||
public const int MaxSlugLength = 100;
|
|
||||||
public const string UserRole = "user";
|
|
||||||
public const string AdminRole = "admin";
|
|
||||||
public const string SysAdminRole = "sys-admin";
|
|
||||||
|
|
||||||
public static readonly FrozenSet<string> AllRoles = FrozenSet.Create(StringComparer.OrdinalIgnoreCase, UserRole, AdminRole, SysAdminRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Index(nameof(Slug), IsUnique = true)]
|
|
||||||
public class CategoryModel : EntityBase, IHasSlug
|
|
||||||
{
|
|
||||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
|
||||||
[Column(TypeName = "citext")]
|
|
||||||
public required string Slug { get; set; }
|
|
||||||
public required string Name { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Index(nameof(Slug), IsUnique = true)]
|
|
||||||
public class PromptModel : EntityBase, IHasSlug
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(Creator))]
|
|
||||||
public Guid CreatorId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual UserModel Creator { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(Category))]
|
|
||||||
public Guid CategoryId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual CategoryModel Category { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(200)]
|
|
||||||
public required string Title { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(4000)]
|
|
||||||
public required string Prompt { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(1000)]
|
|
||||||
public required string Description { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
|
||||||
[Column(TypeName = "citext")]
|
|
||||||
public required string Slug { get; set; }
|
|
||||||
|
|
||||||
[ForeignKey(nameof(SubscriptionTier))]
|
|
||||||
public Guid? SubscriptionTierId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.SetNull)]
|
|
||||||
public virtual SubscriptionTierModel? SubscriptionTier { get; set; }
|
|
||||||
public virtual IList<ReviewModel> Reviews { get; set; } = new List<ReviewModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[PrimaryKey(nameof(ReviewerId), nameof(PromptId))]
|
|
||||||
public class ReviewModel : ITrackableEntity
|
|
||||||
{
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime UpdatedAt { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(Reviewer))]
|
|
||||||
public Guid ReviewerId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual UserModel Reviewer { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(Prompt))]
|
|
||||||
public Guid PromptId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual PromptModel Prompt { get; set; }
|
|
||||||
|
|
||||||
[Range(1, 5)]
|
|
||||||
public int Rating { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(200)]
|
|
||||||
public string? Comment { get; set; } = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Table("UserSubscriptions")]
|
|
||||||
[PrimaryKey(nameof(SubscriberId), nameof(SubscribedToId))]
|
|
||||||
public class SubscriptionModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(SubscribedTo))]
|
|
||||||
public Guid SubscribedToId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual UserModel SubscribedTo { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(Subscriber))]
|
|
||||||
public Guid SubscriberId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual UserModel Subscriber { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
[ForeignKey(nameof(SubscriptionTier))]
|
|
||||||
public virtual Guid? SubscriptionTierId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.SetNull)]
|
|
||||||
public virtual SubscriptionTierModel? SubscriptionTier { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Index(nameof(Level), nameof(UserId), IsUnique = true)]
|
|
||||||
public class SubscriptionTierModel : EntityBase
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public required string Name { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(1000)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[ForeignKey(nameof(User))]
|
|
||||||
public Guid UserId { get; set; }
|
|
||||||
|
|
||||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
|
||||||
public virtual UserModel User { get; set; }
|
|
||||||
|
|
||||||
public decimal MonthlyPrice { get; set; }
|
|
||||||
public int Level { get; set; }
|
|
||||||
|
|
||||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
|
||||||
public virtual IList<SubscriptionModel> Subscriptions { get; set; } = new List<SubscriptionModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Index(nameof(Email), IsUnique = true)]
|
|
||||||
[Index(nameof(UserName), IsUnique = true)]
|
|
||||||
public class UserModel : EntityBase
|
|
||||||
{
|
|
||||||
[MaxLength(100)]
|
|
||||||
[Column(TypeName = "citext")]
|
|
||||||
public required string UserName { get; set; }
|
|
||||||
|
|
||||||
[Column(TypeName = "citext")]
|
|
||||||
public required string Email { get; set; }
|
|
||||||
public required string PasswordHash { get; set; }
|
|
||||||
public required List<string> Roles { get; set; } = new List<string>();
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public required virtual UserProfileModel Profile { get; set; }
|
|
||||||
|
|
||||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
|
||||||
public virtual IList<SubscriptionModel> Subscriptions { get; set; } = new List<SubscriptionModel>();
|
|
||||||
public virtual IList<SubscriptionModel> Subscribers { get; set; } = new List<SubscriptionModel>();
|
|
||||||
public virtual IList<SubscriptionTierModel> SubscriptionTiers { get; set; } = new List<SubscriptionTierModel>();
|
|
||||||
|
|
||||||
public bool IsLockoutEnabled { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database.Models
|
|
||||||
{
|
|
||||||
[Index(nameof(Slug), IsUnique = true)]
|
|
||||||
public class UserProfileModel : EntityBase, IHasSlug
|
|
||||||
{
|
|
||||||
[MaxLength(100)]
|
|
||||||
public required string DisplayName { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
|
||||||
[Column(TypeName = "citext")]
|
|
||||||
public required string Slug { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(2000)]
|
|
||||||
public string? Bio { get; set; }
|
|
||||||
|
|
||||||
public required string AvatarUrl { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(200)]
|
|
||||||
public string? Specialities { get; set; }
|
|
||||||
|
|
||||||
public virtual UserModel User { get; set; }
|
|
||||||
public bool IsPublic { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Database
|
|
||||||
{
|
|
||||||
public class OnlyPromptContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<UserModel> Users { get; set; }
|
|
||||||
public DbSet<UserProfileModel> UserProfiles { get; set; }
|
|
||||||
public DbSet<CategoryModel> Categories { get; set; }
|
|
||||||
public DbSet<PromptModel> Prompts { get; set; }
|
|
||||||
public DbSet<SubscriptionTierModel> SubscriptionTiers { get; set; }
|
|
||||||
public DbSet<SubscriptionModel> Subscriptions { get; set; }
|
|
||||||
public DbSet<ReviewModel> Reviews { get; set; }
|
|
||||||
|
|
||||||
public OnlyPromptContext(DbContextOptions<OnlyPromptContext> options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleEntityTimestamps()
|
|
||||||
{
|
|
||||||
var entries = ChangeTracker.Entries<ITrackableEntity>();
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (entry.State == EntityState.Added)
|
|
||||||
{
|
|
||||||
entry.Entity.CreatedAt = now;
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
else if (entry.State == EntityState.Modified)
|
|
||||||
{
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
HandleEntityTimestamps();
|
|
||||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
||||||
{
|
|
||||||
HandleEntityTimestamps();
|
|
||||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
{
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
if(optionsBuilder.IsConfigured == false)
|
|
||||||
{
|
|
||||||
optionsBuilder.UseNpgsql();
|
|
||||||
optionsBuilder.UseLazyLoadingProxies();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
modelBuilder.Entity<UserModel>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasOne(e => e.Profile)
|
|
||||||
.WithOne(p => p.User)
|
|
||||||
.HasForeignKey<UserProfileModel>(p => p.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity<SubscriptionModel>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasOne(e => e.Subscriber)
|
|
||||||
.WithMany(s => s.Subscriptions)
|
|
||||||
.HasForeignKey(e => e.SubscriberId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.SubscribedTo)
|
|
||||||
.WithMany(c => c.Subscribers)
|
|
||||||
.HasForeignKey(e => e.SubscribedToId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.SubscriptionTier)
|
|
||||||
.WithMany(t => t.Subscriptions)
|
|
||||||
.HasForeignKey(e => e.SubscriptionTierId)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
|
||||||
|
|
||||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
|
||||||
USER $APP_UID
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
|
|
||||||
|
|
||||||
# This stage is used to build the service project
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["OnlyPrompt.Backend/OnlyPrompt.Backend.csproj", "OnlyPrompt.Backend/"]
|
|
||||||
ADD ["OnlyPrompt.Frontend", "OnlyPrompt.Backend/wwwroot"]
|
|
||||||
RUN dotnet restore "./OnlyPrompt.Backend/OnlyPrompt.Backend.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/OnlyPrompt.Backend"
|
|
||||||
RUN dotnet build "./OnlyPrompt.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
# This stage is used to publish the service project to be copied to the final stage
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./OnlyPrompt.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "OnlyPrompt.Backend.dll"]
|
|
||||||
@ -1,419 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(OnlyPromptContext))]
|
|
||||||
[Migration("20260411191205_Initial")]
|
|
||||||
partial class Initial
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
|
||||||
.HasAnnotation("Proxies:CheckEquality", false)
|
|
||||||
.HasAnnotation("Proxies:LazyLoading", true)
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Categories");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("CategoryId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Content")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("CreatorId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CategoryId");
|
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Comment")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("PromptId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<Guid>("ReviewerId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PromptId");
|
|
||||||
|
|
||||||
b.HasIndex("ReviewerId", "PromptId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("SubscriberId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("SubscribedToId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("SubscriberId", "SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("UserSubscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(1000)
|
|
||||||
.HasColumnType("character varying(1000)");
|
|
||||||
|
|
||||||
b.Property<int>("Level")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<decimal>("MonthlyPrice")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.HasIndex("Level", "UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SubscriptionTiers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<bool>("IsLockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("Roles")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Email")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("AvatarUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
|
||||||
.HasMaxLength(2000)
|
|
||||||
.HasColumnType("character varying(2000)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsPublic")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<string>("Specialities")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("UserProfiles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.CategoryModel", "Category")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CategoryId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Creator")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CreatorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("Category");
|
|
||||||
|
|
||||||
b.Navigation("Creator");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.PromptModel", "Prompt")
|
|
||||||
.WithMany("Reviews")
|
|
||||||
.HasForeignKey("PromptId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Reviewer")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ReviewerId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompt");
|
|
||||||
|
|
||||||
b.Navigation("Reviewer");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "SubscribedTo")
|
|
||||||
.WithMany("Subscribers")
|
|
||||||
.HasForeignKey("SubscribedToId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Subscriber")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriberId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("SubscribedTo");
|
|
||||||
|
|
||||||
b.Navigation("Subscriber");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithOne("Profile")
|
|
||||||
.HasForeignKey("OnlyPrompt.Backend.Database.Models.UserProfileModel", "Id")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Profile")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscribers");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,300 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Initial : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterDatabase()
|
|
||||||
.Annotation("Npgsql:PostgresExtension:citext", ",,");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Categories",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
UserName = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
|
||||||
Email = table.Column<string>(type: "citext", nullable: false),
|
|
||||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Roles = table.Column<string[]>(type: "text[]", nullable: false),
|
|
||||||
IsLockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SubscriptionTiers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
|
||||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
MonthlyPrice = table.Column<decimal>(type: "numeric", nullable: false),
|
|
||||||
Level = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SubscriptionTiers", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SubscriptionTiers_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserProfiles",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
|
||||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
|
||||||
Bio = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
|
||||||
AvatarUrl = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Specialities = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
|
||||||
IsPublic = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserProfiles", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserProfiles_Users_Id",
|
|
||||||
column: x => x.Id,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Prompts",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CategoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
|
||||||
Content = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
|
||||||
SubscriptionTierId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Prompts", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Prompts_Categories_CategoryId",
|
|
||||||
column: x => x.CategoryId,
|
|
||||||
principalTable: "Categories",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Prompts_SubscriptionTiers_SubscriptionTierId",
|
|
||||||
column: x => x.SubscriptionTierId,
|
|
||||||
principalTable: "SubscriptionTiers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.SetNull);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Prompts_Users_CreatorId",
|
|
||||||
column: x => x.CreatorId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserSubscriptions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
SubscribedToId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
SubscriberId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
SubscriptionTierId = table.Column<Guid>(type: "uuid", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserSubscriptions", x => new { x.SubscriberId, x.SubscribedToId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserSubscriptions_SubscriptionTiers_SubscriptionTierId",
|
|
||||||
column: x => x.SubscriptionTierId,
|
|
||||||
principalTable: "SubscriptionTiers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.SetNull);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserSubscriptions_Users_SubscribedToId",
|
|
||||||
column: x => x.SubscribedToId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserSubscriptions_Users_SubscriberId",
|
|
||||||
column: x => x.SubscriberId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Reviews",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
ReviewerId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
PromptId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Rating = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Comment = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Reviews", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Reviews_Prompts_PromptId",
|
|
||||||
column: x => x.PromptId,
|
|
||||||
principalTable: "Prompts",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Reviews_Users_ReviewerId",
|
|
||||||
column: x => x.ReviewerId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Categories_Slug",
|
|
||||||
table: "Categories",
|
|
||||||
column: "Slug",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Prompts_CategoryId",
|
|
||||||
table: "Prompts",
|
|
||||||
column: "CategoryId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Prompts_CreatorId",
|
|
||||||
table: "Prompts",
|
|
||||||
column: "CreatorId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Prompts_Slug",
|
|
||||||
table: "Prompts",
|
|
||||||
column: "Slug",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Prompts_SubscriptionTierId",
|
|
||||||
table: "Prompts",
|
|
||||||
column: "SubscriptionTierId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Reviews_PromptId",
|
|
||||||
table: "Reviews",
|
|
||||||
column: "PromptId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Reviews_ReviewerId_PromptId",
|
|
||||||
table: "Reviews",
|
|
||||||
columns: new[] { "ReviewerId", "PromptId" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SubscriptionTiers_Level_UserId",
|
|
||||||
table: "SubscriptionTiers",
|
|
||||||
columns: new[] { "Level", "UserId" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SubscriptionTiers_UserId",
|
|
||||||
table: "SubscriptionTiers",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserProfiles_Slug",
|
|
||||||
table: "UserProfiles",
|
|
||||||
column: "Slug",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Users_Email",
|
|
||||||
table: "Users",
|
|
||||||
column: "Email",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Users_UserName",
|
|
||||||
table: "Users",
|
|
||||||
column: "UserName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserSubscriptions_SubscribedToId",
|
|
||||||
table: "UserSubscriptions",
|
|
||||||
column: "SubscribedToId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserSubscriptions_SubscriptionTierId",
|
|
||||||
table: "UserSubscriptions",
|
|
||||||
column: "SubscriptionTierId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Reviews");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserProfiles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserSubscriptions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Prompts");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Categories");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SubscriptionTiers");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,419 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(OnlyPromptContext))]
|
|
||||||
[Migration("20260412002927_ReviewManyToMany")]
|
|
||||||
partial class ReviewManyToMany
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
|
||||||
.HasAnnotation("Proxies:CheckEquality", false)
|
|
||||||
.HasAnnotation("Proxies:LazyLoading", true)
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Categories");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("CategoryId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("CreatorId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1000)
|
|
||||||
.HasColumnType("character varying(1000)");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4000)
|
|
||||||
.HasColumnType("character varying(4000)");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CategoryId");
|
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("ReviewerId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("PromptId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Comment")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("ReviewerId", "PromptId");
|
|
||||||
|
|
||||||
b.HasIndex("PromptId");
|
|
||||||
|
|
||||||
b.ToTable("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("SubscriberId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("SubscribedToId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("SubscriberId", "SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("UserSubscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(1000)
|
|
||||||
.HasColumnType("character varying(1000)");
|
|
||||||
|
|
||||||
b.Property<int>("Level")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<decimal>("MonthlyPrice")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.HasIndex("Level", "UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SubscriptionTiers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<bool>("IsLockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<List<string>>("Roles")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Email")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("AvatarUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
|
||||||
.HasMaxLength(2000)
|
|
||||||
.HasColumnType("character varying(2000)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsPublic")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<string>("Specialities")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("UserProfiles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.CategoryModel", "Category")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CategoryId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Creator")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CreatorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("Category");
|
|
||||||
|
|
||||||
b.Navigation("Creator");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.PromptModel", "Prompt")
|
|
||||||
.WithMany("Reviews")
|
|
||||||
.HasForeignKey("PromptId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Reviewer")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ReviewerId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompt");
|
|
||||||
|
|
||||||
b.Navigation("Reviewer");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "SubscribedTo")
|
|
||||||
.WithMany("Subscribers")
|
|
||||||
.HasForeignKey("SubscribedToId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Subscriber")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriberId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("SubscribedTo");
|
|
||||||
|
|
||||||
b.Navigation("Subscriber");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithOne("Profile")
|
|
||||||
.HasForeignKey("OnlyPrompt.Backend.Database.Models.UserProfileModel", "Id")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Profile")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscribers");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class ReviewManyToMany : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_Reviews",
|
|
||||||
table: "Reviews");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Reviews_ReviewerId_PromptId",
|
|
||||||
table: "Reviews");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Id",
|
|
||||||
table: "Reviews");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Content",
|
|
||||||
table: "Prompts");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Description",
|
|
||||||
table: "Prompts",
|
|
||||||
type: "character varying(1000)",
|
|
||||||
maxLength: 1000,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Prompt",
|
|
||||||
table: "Prompts",
|
|
||||||
type: "character varying(4000)",
|
|
||||||
maxLength: 4000,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_Reviews",
|
|
||||||
table: "Reviews",
|
|
||||||
columns: new[] { "ReviewerId", "PromptId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_Reviews",
|
|
||||||
table: "Reviews");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Description",
|
|
||||||
table: "Prompts");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Prompt",
|
|
||||||
table: "Prompts");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<Guid>(
|
|
||||||
name: "Id",
|
|
||||||
table: "Reviews",
|
|
||||||
type: "uuid",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Content",
|
|
||||||
table: "Prompts",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_Reviews",
|
|
||||||
table: "Reviews",
|
|
||||||
column: "Id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Reviews_ReviewerId_PromptId",
|
|
||||||
table: "Reviews",
|
|
||||||
columns: new[] { "ReviewerId", "PromptId" },
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,416 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(OnlyPromptContext))]
|
|
||||||
partial class OnlyPromptContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
|
||||||
.HasAnnotation("Proxies:CheckEquality", false)
|
|
||||||
.HasAnnotation("Proxies:LazyLoading", true)
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Categories");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("CategoryId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("CreatorId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1000)
|
|
||||||
.HasColumnType("character varying(1000)");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4000)
|
|
||||||
.HasColumnType("character varying(4000)");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CategoryId");
|
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("ReviewerId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("PromptId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Comment")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("ReviewerId", "PromptId");
|
|
||||||
|
|
||||||
b.HasIndex("PromptId");
|
|
||||||
|
|
||||||
b.ToTable("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("SubscriberId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("SubscribedToId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid?>("SubscriptionTierId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("SubscriberId", "SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscribedToId");
|
|
||||||
|
|
||||||
b.HasIndex("SubscriptionTierId");
|
|
||||||
|
|
||||||
b.ToTable("UserSubscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(1000)
|
|
||||||
.HasColumnType("character varying(1000)");
|
|
||||||
|
|
||||||
b.Property<int>("Level")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<decimal>("MonthlyPrice")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.HasIndex("Level", "UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SubscriptionTiers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<bool>("IsLockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<List<string>>("Roles")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Email")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("AvatarUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
|
||||||
.HasMaxLength(2000)
|
|
||||||
.HasColumnType("character varying(2000)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsPublic")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("citext");
|
|
||||||
|
|
||||||
b.Property<string>("Specialities")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("UserProfiles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.CategoryModel", "Category")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CategoryId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Creator")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("CreatorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Prompts")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("Category");
|
|
||||||
|
|
||||||
b.Navigation("Creator");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.PromptModel", "Prompt")
|
|
||||||
.WithMany("Reviews")
|
|
||||||
.HasForeignKey("PromptId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Reviewer")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ReviewerId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompt");
|
|
||||||
|
|
||||||
b.Navigation("Reviewer");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "SubscribedTo")
|
|
||||||
.WithMany("Subscribers")
|
|
||||||
.HasForeignKey("SubscribedToId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Subscriber")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriberId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
|
||||||
.WithMany("Subscriptions")
|
|
||||||
.HasForeignKey("SubscriptionTierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("SubscribedTo");
|
|
||||||
|
|
||||||
b.Navigation("Subscriber");
|
|
||||||
|
|
||||||
b.Navigation("SubscriptionTier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
|
||||||
.WithOne("Profile")
|
|
||||||
.HasForeignKey("OnlyPrompt.Backend.Database.Models.UserProfileModel", "Id")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Reviews");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Profile")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Prompts");
|
|
||||||
|
|
||||||
b.Navigation("Subscribers");
|
|
||||||
|
|
||||||
b.Navigation("Subscriptions");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" NoWarn="NU1605" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" NoWarn="NU1605" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.1" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="3.14.1" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.13.22" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore.Microsoft" Version="2.13.22" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Migrations\" />
|
|
||||||
<Folder Include="wwwroot\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
@OnlyPrompt.Backend_HostAddress = http://localhost:5093
|
|
||||||
|
|
||||||
GET {{OnlyPrompt.Backend_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Rewrite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.Identity.Web;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using OnlyPrompt.Backend.Database;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Services.Jwt;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using Scalar.AspNetCore;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
var config = builder.Configuration;
|
|
||||||
// Add services to the container.
|
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
|
|
||||||
builder.Services.AddDbContext<OnlyPromptContext>(opts =>
|
|
||||||
{
|
|
||||||
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
|
|
||||||
opts.UseLazyLoadingProxies();
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<IPasswordHasher<UserModel>, PasswordHasher<UserModel>>();
|
|
||||||
builder.Services.AddSingleton<ITokenService, JwtTokenService>();
|
|
||||||
builder.Services.AddAutoMapper(AutoMapperSetup.Setup);
|
|
||||||
builder.Services.AddValidation(opts =>
|
|
||||||
{
|
|
||||||
opts.MaxDepth = 10;
|
|
||||||
});
|
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
||||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opts =>
|
|
||||||
{
|
|
||||||
opts.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidateLifetime = true,
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
ValidIssuer = config["Jwt:Issuer"],
|
|
||||||
ValidAudience = config["Jwt:Audience"],
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]))
|
|
||||||
};
|
|
||||||
opts.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
if (context.Request.Cookies.ContainsKey("jwt"))
|
|
||||||
context.Token = context.Request.Cookies["jwt"];
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddControllers().AddJsonOptions(jsonOpts =>
|
|
||||||
{
|
|
||||||
jsonOpts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
|
||||||
});
|
|
||||||
builder.Services.AddOpenApi(opts => opts.AddScalarTransformers());
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
app.MapScalarApiReference();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
var rewrite = new RewriteOptions()
|
|
||||||
.AddRewrite(@"^(?!scalar\/?|api\/?)([^.]+)$", "$1.html", skipRemainingRules: true);
|
|
||||||
|
|
||||||
app.UseRewriter(rewrite);
|
|
||||||
app.UseAuthorization();
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
var dir = Path.GetFullPath("./../OnlyPrompt.Frontend");
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
|
||||||
{
|
|
||||||
FileProvider = new PhysicalFileProvider(dir),
|
|
||||||
RedirectToAppendTrailingSlash = true,
|
|
||||||
HttpsCompression = HttpsCompressionMode.Compress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
app.UseStaticFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
app.MapFallbackToFile("/login.html");
|
|
||||||
using var scope = app.Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<OnlyPromptContext>();
|
|
||||||
await db.Database.MigrateAsync();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "https://localhost:7163/scalar",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"applicationUrl": "https://localhost:7163;http://localhost:5093"
|
|
||||||
},
|
|
||||||
"Container (Dockerfile)": {
|
|
||||||
"commandName": "Docker",
|
|
||||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
|
||||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
|
||||||
},
|
|
||||||
"publishAllPorts": true,
|
|
||||||
"useSSL": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json"
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Services.Jwt
|
|
||||||
{
|
|
||||||
public interface ITokenService
|
|
||||||
{
|
|
||||||
string BuildToken(UserModel user, out DateTime validUntil);
|
|
||||||
bool ValidateToken(string token, out ClaimsPrincipal claims);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using OnlyPrompt.Backend.Utils;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Services.Jwt
|
|
||||||
{
|
|
||||||
public class JwtTokenService : ITokenService
|
|
||||||
{
|
|
||||||
private string _key;
|
|
||||||
private string _issuer;
|
|
||||||
private string _audience;
|
|
||||||
private TimeSpan _valid;
|
|
||||||
|
|
||||||
public JwtTokenService(IConfiguration config)
|
|
||||||
{
|
|
||||||
config = config.GetSection("Jwt");
|
|
||||||
_key = config["Key"];
|
|
||||||
_issuer = config["Issuer"];
|
|
||||||
_audience = config["Audience"];
|
|
||||||
_valid = config.GetValue<TimeSpan>("Valid");
|
|
||||||
}
|
|
||||||
|
|
||||||
public string BuildToken(UserModel user, out DateTime validUntil)
|
|
||||||
{
|
|
||||||
var claims = user.GetClaims().ToList();
|
|
||||||
validUntil = DateTime.UtcNow.Add(_valid);
|
|
||||||
claims.Add(new Claim("exp", new DateTimeOffset(validUntil).ToUnixTimeSeconds().ToString()));
|
|
||||||
claims.Add(new Claim("amr", "pwd"));
|
|
||||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_key));
|
|
||||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
|
|
||||||
var tokenDescriptor = new JwtSecurityToken(_issuer, _audience, claims, expires: validUntil, signingCredentials: credentials);
|
|
||||||
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ValidateToken(string token, out ClaimsPrincipal claims)
|
|
||||||
{
|
|
||||||
var mySecret = Encoding.UTF8.GetBytes(_key);
|
|
||||||
var mySecurityKey = new SymmetricSecurityKey(mySecret);
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
claims = tokenHandler.ValidateToken(token,
|
|
||||||
new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidIssuer = _issuer,
|
|
||||||
ValidAudience = _audience,
|
|
||||||
IssuerSigningKey = mySecurityKey,
|
|
||||||
}, out SecurityToken validatedToken);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
claims = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Auth;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Category;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Prompt;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.Subscription;
|
|
||||||
using OnlyPrompt.Backend.ApiModels.UserProfile;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public static class AutoMapperSetup
|
|
||||||
{
|
|
||||||
public static void Setup(IMapperConfigurationExpression config)
|
|
||||||
{
|
|
||||||
config.CreateMap<UserModel, ApiUser>()
|
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
|
||||||
.MapCtorParamFrom(x => x.UserName, x => x.UserName)
|
|
||||||
.MapCtorParamFrom(x => x.Roles, x => x.Roles)
|
|
||||||
.MapCtorParamFrom(x => x.Email, x => x.Email);
|
|
||||||
|
|
||||||
config.CreateMap<UserProfileModel, ApiUserProfile>()
|
|
||||||
.MapCtorParamFrom(x => x.DisplayName, x => x.DisplayName)
|
|
||||||
.MapCtorParamFrom(x => x.Slug, x => x.Slug)
|
|
||||||
.MapCtorParamFrom(x => x.Bio, x => x.Bio)
|
|
||||||
.MapCtorParamFrom(x => x.AvatarUrl, x => x.AvatarUrl)
|
|
||||||
.MapCtorParamFrom(x => x.Specialities, x => x.Specialities)
|
|
||||||
.MapCtorParamFrom(x => x.AverageRating, x => x.User.Prompts.Average(p => p.Reviews.Average(r => r.Rating)))
|
|
||||||
.MapCtorParamFrom(x => x.Subscribers, x => x.User.Subscribers.Count());
|
|
||||||
|
|
||||||
config.CreateMap<PromptModel, ApiPrompt>()
|
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
|
||||||
.MapCtorParamFrom(x => x.Title, x => x.Title)
|
|
||||||
.MapCtorParamFrom(x => x.Description, x => x.Description)
|
|
||||||
.MapCtorParamFrom(x => x.Content, x => x.Prompt)
|
|
||||||
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
|
||||||
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
|
||||||
.MapCtorParamFrom(x => x.TierName, x => x.SubscriptionTier == null ? null : x.SubscriptionTier.Name)
|
|
||||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
|
||||||
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
|
||||||
.MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating));
|
|
||||||
|
|
||||||
config.CreateMap<PromptModel, ApiMinimalPrompt>()
|
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
|
||||||
.MapCtorParamFrom(x => x.Title, x => x.Title)
|
|
||||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
|
||||||
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
|
||||||
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
|
||||||
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
|
||||||
.MapCtorParamFrom(x => x.TierName, x => x.SubscriptionTier == null ? null : x.SubscriptionTier.Name)
|
|
||||||
.MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating))
|
|
||||||
.MapCtorParamFrom(x => x.CanAccess, x => true);
|
|
||||||
|
|
||||||
config.CreateMap<ReviewModel, ApiReview>()
|
|
||||||
.MapCtorParamFrom(x => x.CreatorId, x => x.ReviewerId)
|
|
||||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Reviewer.Profile.DisplayName)
|
|
||||||
.MapCtorParamFrom(x => x.Comment, x => x.Comment)
|
|
||||||
.MapCtorParamFrom(x => x.Rating, x => x.Rating);
|
|
||||||
|
|
||||||
config.CreateMap<CategoryModel, ApiCategory>()
|
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
|
||||||
.MapCtorParamFrom(x => x.Name, x => x.Name)
|
|
||||||
.MapCtorParamFrom(x => x.Description, x => x.Description)
|
|
||||||
.MapCtorParamFrom(x => x.Slug, x => x.Slug);
|
|
||||||
|
|
||||||
config.CreateMap<CategoryModel, ApiMinimalCategory>()
|
|
||||||
.MapCtorParamFrom(x => x.Name, x => x.Name)
|
|
||||||
.MapCtorParamFrom(x => x.Slug, x => x.Slug);
|
|
||||||
|
|
||||||
config.CreateMap<ApiCreateCategoryRequest, CategoryModel>()
|
|
||||||
.MapMemberFrom(x => x.Description, x => x.Description)
|
|
||||||
.MapMemberFrom(x => x.Name, x => x.Name)
|
|
||||||
.MapMemberFrom(x => x.Slug, x => x.Slug);
|
|
||||||
|
|
||||||
config.CreateMap<SubscriptionTierModel, ApiSubscriptionTier>()
|
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
|
||||||
.MapCtorParamFrom(x => x.Name, x => x.Name)
|
|
||||||
.MapCtorParamFrom(x => x.Level, x => x.Level)
|
|
||||||
.MapCtorParamFrom(x => x.MonthlyPrice, x => x.MonthlyPrice)
|
|
||||||
.MapCtorParamFrom(x => x.Description, x => x.Description);
|
|
||||||
|
|
||||||
config.CreateMap<ApiCreateSubscriptionTierRequest, SubscriptionTierModel>()
|
|
||||||
.MapMemberFrom(x => x.Name, x => x.Name)
|
|
||||||
.MapMemberFrom(x => x.Level, x => x.Level)
|
|
||||||
.MapMemberFrom(x => x.MonthlyPrice, x => x.MonthlyPrice)
|
|
||||||
.MapMemberFrom(x => x.Description, x => x.Description);
|
|
||||||
|
|
||||||
config.CreateMap<SubscriptionModel, ApiSubscription>()
|
|
||||||
.MapCtorParamFrom(x => x.SubscribedToId, x => x.SubscribedToId)
|
|
||||||
.MapCtorParamFrom(x => x.SubscribedToName, x => x.SubscribedTo.Profile.DisplayName)
|
|
||||||
.MapCtorParamFrom(x => x.CurrentTier, x => x.SubscriptionTier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.Configuration;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public static class AutomapperExtensions
|
|
||||||
{
|
|
||||||
|
|
||||||
public static IMappingExpression<TSource, TDestination> CreateUpdateMap<TSource, TDestination>(this IMapperConfigurationExpression cfg, MemberList memberList = MemberList.Source)
|
|
||||||
{
|
|
||||||
return cfg.CreateMap<TSource, TDestination>(memberList)
|
|
||||||
.IgnoreNullMembers();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IMappingExpression<TSource, TDestination> MapMemberFrom<TSource, TDestination, TMember, TSourceMember>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TSourceMember>> sourceMember)
|
|
||||||
{
|
|
||||||
mapping.ForMember(destinationMember, x => x.MapFrom(sourceMember));
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IMappingExpression<TSource, TDestination> IgnoreNullMembers<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mapping)
|
|
||||||
{
|
|
||||||
mapping.ForAllMembers(opts => opts.Condition((src, dest, member) => src != null));
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IMappingExpression<TSource, TDestination> MapCtorParamFrom<TSource, TDestination, TMember, TSourceMember>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TSourceMember>> sourceMember)
|
|
||||||
{
|
|
||||||
mapping.ForCtorParam(destinationMember, x => x.MapFrom(sourceMember));
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IMappingExpression<TSource, TDestination> ForCtorParam<TSource, TDestination, DValue>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, DValue>> paramSelector, Action<ICtorParamConfigurationExpression<TSource>> configure)
|
|
||||||
{
|
|
||||||
var ctorParamName = ((MemberExpression)paramSelector.Body).Member.Name;
|
|
||||||
mapping.ForCtorParam(ctorParamName, configure);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OnlyPrompt.Backend.Database.Core;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public static class EntityExtensions
|
|
||||||
{
|
|
||||||
public static Task<T?> FindBySlugAsync<T>(this IQueryable<T> queryable, string slug) where T : class, IHasSlug
|
|
||||||
{
|
|
||||||
return queryable.FirstOrDefaultAsync(e => e.Slug == slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T?> FindByIdentifierAsync<T>(this IQueryable<T> queryable, Identifier identifier) where T : class, IHasSlug, IEntity
|
|
||||||
{
|
|
||||||
if (identifier.Id.HasValue)
|
|
||||||
return queryable.FirstOrDefaultAsync(e => e.Id == identifier.Id.Value);
|
|
||||||
|
|
||||||
return queryable.FindBySlugAsync(identifier.Slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> OfIdentifer<T>(this IQueryable<T> queryable, Identifier identifier) where T : class, IHasSlug, IEntity
|
|
||||||
{
|
|
||||||
if (identifier.Id.HasValue)
|
|
||||||
return queryable.Where(e => e.Id == identifier.Id.Value);
|
|
||||||
return queryable.Where(e => e.Slug == identifier.Slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public static class Extensions
|
|
||||||
{
|
|
||||||
public static string? GetIdentifier(this ClaimsPrincipal principal) => principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
public static bool TryGetIdentifier(this ClaimsPrincipal principal, [NotNullWhen(true)]out string? identifier)
|
|
||||||
{
|
|
||||||
identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
return identifier != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Guid? GetUserId(this ClaimsPrincipal principal)
|
|
||||||
{
|
|
||||||
if (principal.TryGetIdentifier(out var identifier) && Guid.TryParse(identifier, out var userId))
|
|
||||||
return userId;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<Claim> GetClaims(this UserModel user)
|
|
||||||
{
|
|
||||||
yield return new Claim(ClaimTypes.NameIdentifier, user.Id.ToString());
|
|
||||||
yield return new Claim(ClaimTypes.Name, user.UserName);
|
|
||||||
yield return new Claim(ClaimTypes.Email, user.Email);
|
|
||||||
|
|
||||||
foreach (var role in user.Roles)
|
|
||||||
yield return new Claim(ClaimTypes.Role, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> OrderBy<T, TKey>(this IQueryable<T> source, Expression<Func<T, TKey>> selecter, bool ascending)
|
|
||||||
{
|
|
||||||
if(ascending)
|
|
||||||
return source.OrderBy(selecter);
|
|
||||||
else
|
|
||||||
return source.OrderByDescending(selecter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CookieOptions Copy(this CookieOptions options, Action<CookieOptions>? modify = null)
|
|
||||||
{
|
|
||||||
var newOptions = new CookieOptions
|
|
||||||
{
|
|
||||||
Domain = options.Domain,
|
|
||||||
Expires = options.Expires,
|
|
||||||
HttpOnly = options.HttpOnly,
|
|
||||||
IsEssential = options.IsEssential,
|
|
||||||
MaxAge = options.MaxAge,
|
|
||||||
Path = options.Path,
|
|
||||||
SameSite = options.SameSite,
|
|
||||||
Secure = options.Secure
|
|
||||||
};
|
|
||||||
|
|
||||||
modify?.Invoke(newOptions);
|
|
||||||
return newOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string Limit(this string @string, int maxLength)
|
|
||||||
{
|
|
||||||
if (@string.Length <= maxLength)
|
|
||||||
return @string;
|
|
||||||
return @string.Substring(0, maxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public const string LowerAlphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
||||||
public static string GetString(this Random @random, int lenght, string alphabet = LowerAlphabet)
|
|
||||||
{
|
|
||||||
Span<char> chars = stackalloc char[lenght];
|
|
||||||
for (int i = 0; i < lenght; i++)
|
|
||||||
chars[i] = alphabet[@random.Next(alphabet.Length)];
|
|
||||||
|
|
||||||
return new string(chars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
using Scalar.AspNetCore;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public struct Identifier
|
|
||||||
{
|
|
||||||
public string Slug { get; init; }
|
|
||||||
public Guid? Id { get; init; }
|
|
||||||
|
|
||||||
public Identifier(string slug)
|
|
||||||
{
|
|
||||||
this.Slug = slug;
|
|
||||||
this.Id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Identifier(Guid guid)
|
|
||||||
{
|
|
||||||
this.Id = guid;
|
|
||||||
this.Slug = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator Identifier(string slug) => new Identifier(slug);
|
|
||||||
public static implicit operator Identifier(Guid guid) => new Identifier(guid);
|
|
||||||
|
|
||||||
public static bool TryParse(string input, out Identifier identifier)
|
|
||||||
{
|
|
||||||
identifier= new Identifier(input);
|
|
||||||
if (Guid.TryParse(input, out var guid))
|
|
||||||
identifier = new Identifier(guid);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Utils
|
|
||||||
{
|
|
||||||
public static class SlugHelper
|
|
||||||
{
|
|
||||||
private static readonly Regex InvalidCharacters = new(@"[^a-z0-9\-]", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex MultipleDashes = new(@"-+", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
public static string GenerateSlug(string input, int? maxLength = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
var slug = input.ToLowerInvariant().Replace(" ", "-").Replace("_", "-");
|
|
||||||
slug = InvalidCharacters.Replace(slug, string.Empty);
|
|
||||||
slug = MultipleDashes.Replace(slug, "-");
|
|
||||||
slug = slug.Trim('-');
|
|
||||||
if (maxLength.HasValue)
|
|
||||||
slug = slug.Limit(maxLength.Value);
|
|
||||||
|
|
||||||
return slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string SuffixChars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
public static async Task<string> GenerateUniqueSlugAsync(string input, Func<string, Task<bool>> existsFunc, int? maxLenght)
|
|
||||||
{
|
|
||||||
var slug = GenerateSlug(input);
|
|
||||||
var exists = await existsFunc(slug);
|
|
||||||
if (exists)
|
|
||||||
{
|
|
||||||
var suffix = Random.Shared.GetString(8, SuffixChars);
|
|
||||||
if (maxLenght.HasValue)
|
|
||||||
slug = slug.Limit(maxLenght.Value - 9);
|
|
||||||
|
|
||||||
slug = $"{slug}-{suffix}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return slug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Include Error Detail=true;User ID=onlyprompt;Password=onlyprompt;Host=localhost;Port=2803;Database=onlyprompt;Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;"
|
|
||||||
},
|
|
||||||
"Jwt": {
|
|
||||||
"Issuer": "https://onlyprompts.com",
|
|
||||||
"Audience": "https://onlyprompts.com",
|
|
||||||
"Key": "TfZi@!CC!b5UoD81gs&%tvY4J0M$p3cI",
|
|
||||||
"Valid": "3.0:0:0"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
16
OnlyPrompt.Frontend/.vscode/launch.json
vendored
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "msedge",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Open current page in Edge",
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"url": "https://localhost:7163/${fileBasename}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
OnlyPrompt.Frontend/.vscode/settings.json
vendored
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.indentSize": 2
|
|
||||||
}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
<!-- OnlyPrompt - Chats page:
|
|
||||||
- Direct messaging interface with conversation list and active chat window -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>OnlyPrompt - Chats</title>
|
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
|
||||||
<link rel="stylesheet" href="../css/chats.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
|
||||||
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div style="flex:1; display: flex; flex-direction: column;">
|
|
||||||
|
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="chats-main">
|
|
||||||
<!-- Chat Container: Left column (list) + Right column (active chat) -->
|
|
||||||
<div class="chat-container">
|
|
||||||
|
|
||||||
<!-- Left Column: Chat Overview -->
|
|
||||||
<div class="chat-list">
|
|
||||||
<div class="chat-list-header">
|
|
||||||
<h2>Messages</h2>
|
|
||||||
<button class="new-chat-btn"><i class="bi bi-pencil-square"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="chat-list-items">
|
|
||||||
<!-- Chat Entry 1 (active) -->
|
|
||||||
<div class="chat-item active">
|
|
||||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar">
|
|
||||||
<div class="chat-item-info">
|
|
||||||
<div class="chat-name">Alex Chen</div>
|
|
||||||
<div class="chat-last-msg">Hey Sarah! Really loved your last video on minimalism...</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-time">10:17 AM</div>
|
|
||||||
</div>
|
|
||||||
<!-- Chat Entry 2 -->
|
|
||||||
<div class="chat-item">
|
|
||||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="chat-avatar">
|
|
||||||
<div class="chat-item-info">
|
|
||||||
<div class="chat-name">Mia Wong</div>
|
|
||||||
<div class="chat-last-msg">Thanks for the prompt tips! They worked perfectly.</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-time">Yesterday</div>
|
|
||||||
</div>
|
|
||||||
<!-- Chat Entry 3 -->
|
|
||||||
<div class="chat-item">
|
|
||||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="chat-avatar">
|
|
||||||
<div class="chat-item-info">
|
|
||||||
<div class="chat-name">Tom Rivera</div>
|
|
||||||
<div class="chat-last-msg">Let's schedule a call for the collab?</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-time">Yesterday</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Active Chat (with Alex Chen) -->
|
|
||||||
<div class="chat-active">
|
|
||||||
<div class="chat-header">
|
|
||||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar-large">
|
|
||||||
<div class="chat-header-info">
|
|
||||||
<div class="chat-header-name">Alex Chen</div>
|
|
||||||
<div class="chat-header-status"><span class="online-dot"></span> Online</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-messages">
|
|
||||||
<!-- Message from Alex -->
|
|
||||||
<div class="message received">
|
|
||||||
<div class="message-bubble">Hey Sarah! Really loved your last video on minimalism. Quick question about your workspace layout?</div>
|
|
||||||
<div class="message-time">10:15 AM</div>
|
|
||||||
</div>
|
|
||||||
<!-- Reply from Sarah -->
|
|
||||||
<div class="message sent">
|
|
||||||
<div class="message-bubble">Thanks Alex! Appreciate it. Yes, happy to share! The desk is from Article, and the shelving unit is custom-built. Highly recommend a clean setup!</div>
|
|
||||||
<div class="message-time">10:16 AM</div>
|
|
||||||
</div>
|
|
||||||
<!-- Alex replies -->
|
|
||||||
<div class="message received">
|
|
||||||
<div class="message-bubble">Thanks so much! Your aesthetic is exactly what I'm aiming for. Can't wait for your next piece!</div>
|
|
||||||
<div class="message-time">10:17 AM</div>
|
|
||||||
</div>
|
|
||||||
<!-- Sarah replies -->
|
|
||||||
<div class="message sent">
|
|
||||||
<div class="message-bubble">Awesome! Let me know if you need more tips. Enjoy the process! 😊</div>
|
|
||||||
<div class="message-time">10:18 AM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-input-area">
|
|
||||||
<input type="text" placeholder="Type your message...">
|
|
||||||
<button class="send-btn">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
fetch('../html/sidebar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
|
||||||
// Remove 'active' from all sidebar links
|
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
|
||||||
// Set 'active' on the Chats link (4th link, index 3)
|
|
||||||
const chatsLink = document.querySelectorAll('#sidebar-container .sidebar li a')[3];
|
|
||||||
if (chatsLink) chatsLink.classList.add('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('../html/topbar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
<!-- OnlyPrompt - Marketplace page:
|
|
||||||
- Browse and filter AI prompts with buy/view details buttons and pricing -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>OnlyPrompt - Discover Creators</title>
|
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
|
||||||
<link rel="stylesheet" href="../css/community.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
|
||||||
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
|
||||||
|
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="creators-main">
|
|
||||||
|
|
||||||
<!-- Header / Titel -->
|
|
||||||
<div class="creators-header">
|
|
||||||
<h1>Discover Creators</h1>
|
|
||||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter Buttons -->
|
|
||||||
<div class="filter-buttons">
|
|
||||||
<button class="filter-btn active">Popular</button>
|
|
||||||
<button class="filter-btn">Rising</button>
|
|
||||||
<button class="filter-btn">New</button>
|
|
||||||
<button class="filter-btn">Top Rated</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creators Grid -->
|
|
||||||
<div class="creators-grid">
|
|
||||||
|
|
||||||
<!-- Creator Card 1 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator1.png" alt="Sarah Jenkins" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Sarah Jenkins</h3>
|
|
||||||
<div class="creator-handle">@sarahj</div>
|
|
||||||
<p class="creator-bio">AI Explorer | Prompt Curator | Exploring creativity through generative AI.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 42 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 2 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Alex Chen</h3>
|
|
||||||
<div class="creator-handle">@alexchen</div>
|
|
||||||
<p class="creator-bio">Digital artist & prompt engineer. Creating surreal landscapes.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 87 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 3 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Mia Wong</h3>
|
|
||||||
<div class="creator-handle">@miawong</div>
|
|
||||||
<p class="creator-bio">Midjourney master | UI/UX prompts | Design systems.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 124 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 5.0</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 4 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Tom Rivera</h3>
|
|
||||||
<div class="creator-handle">@tomrivera</div>
|
|
||||||
<p class="creator-bio">3D artist | Character design | Sci-fi & fantasy prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 33 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 5 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator5.png" alt="Emma Watson" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Emma Watson</h3>
|
|
||||||
<div class="creator-handle">@emmawatson</div>
|
|
||||||
<p class="creator-bio">Watercolor & pet portraits | Whimsical art prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 56 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 6 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator6.png" alt="Liam O'Brien" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Liam O'Brien</h3>
|
|
||||||
<div class="creator-handle">@liamob</div>
|
|
||||||
<p class="creator-bio">Minimalist logo designer | Brand identity prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 28 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.6</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
fetch('../html/sidebar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
|
||||||
// Remove 'active' from all sidebar links
|
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
|
||||||
// Set 'active' on the third link (Community)
|
|
||||||
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
|
|
||||||
if (thirdLink) thirdLink.classList.add('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('../html/topbar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
<!-- OnlyPrompt - Create Prompt page:
|
|
||||||
- Form to publish new AI prompts with title, description, category, content, example output, image upload, and pricing toggle -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>OnlyPrompt - Create New Prompt</title>
|
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
|
||||||
<link rel="stylesheet" href="../css/create.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
|
||||||
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div style="flex:1; display: flex; flex-direction: column;">
|
|
||||||
|
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="create-main">
|
|
||||||
<div class="create-container">
|
|
||||||
<div class="create-header">
|
|
||||||
<h1>Create AI Prompt</h1>
|
|
||||||
<p>Design and save custom prompts for your AI workflows.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="createPromptForm" class="create-form" enctype="multipart/form-data">
|
|
||||||
<!-- Title -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="title">Prompt Title *</label>
|
|
||||||
<input type="text" id="title" name="title" placeholder="e.g., Write an inspiring startup story about innovation" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="description">Description *</label>
|
|
||||||
<textarea id="description" name="description" rows="2" placeholder="Draft a narrative about a small team overcoming challenges to launch a groundbreaking app" required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Category -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="category">Category *</label>
|
|
||||||
<select id="category" name="category" required>
|
|
||||||
<option value="creative-writing">Creative Writing</option>
|
|
||||||
<option value="coding">Coding</option>
|
|
||||||
<option value="art">Art</option>
|
|
||||||
<option value="marketing">Marketing</option>
|
|
||||||
<option value="video">Video</option>
|
|
||||||
<option value="data">Data</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Prompt Content -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="promptContent">Prompt Content *</label>
|
|
||||||
<textarea id="promptContent" name="promptContent" rows="6" placeholder="Write your prompt instructions here..." required></textarea>
|
|
||||||
<small class="form-hint">Use clear, step-by-step instructions for the AI.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Example Output (Text) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="exampleOutput">Example Output (optional)</label>
|
|
||||||
<textarea id="exampleOutput" name="exampleOutput" rows="4" placeholder="Show an example of what the AI might generate..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Example Image (optional) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="exampleImage">Example Image (optional)</label>
|
|
||||||
<input type="file" id="exampleImage" name="exampleImage" accept="image/png, image/jpeg, image/jpg">
|
|
||||||
<small class="form-hint">Upload a PNG or JPG – preview will appear below.</small>
|
|
||||||
<div id="imagePreview" style="margin-top: 10px; display: none;">
|
|
||||||
<img id="previewImg" src="#" alt="Preview" style="max-width: 100%; max-height: 200px; border-radius: 12px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pricing (with toggle) -->
|
|
||||||
<div class="form-group pricing-group">
|
|
||||||
<label>Pricing</label>
|
|
||||||
<div class="pricing-toggle">
|
|
||||||
<button type="button" id="freeBtn" class="price-option active">Free</button>
|
|
||||||
<button type="button" id="paidBtn" class="price-option">Paid</button>
|
|
||||||
</div>
|
|
||||||
<div id="priceField" style="display: none;">
|
|
||||||
<input type="number" id="price" name="price" step="0.01" min="0" placeholder="Price in USD (e.g., 19.99)">
|
|
||||||
</div>
|
|
||||||
<small class="form-hint">You can set a price later or keep it free.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="submit-btn">Publish Prompt</button>
|
|
||||||
<button type="button" class="cancel-btn">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Toggle between free and paid
|
|
||||||
const freeBtn = document.getElementById('freeBtn');
|
|
||||||
const paidBtn = document.getElementById('paidBtn');
|
|
||||||
const priceField = document.getElementById('priceField');
|
|
||||||
const priceInput = document.getElementById('price');
|
|
||||||
|
|
||||||
freeBtn.addEventListener('click', () => {
|
|
||||||
freeBtn.classList.add('active');
|
|
||||||
paidBtn.classList.remove('active');
|
|
||||||
priceField.style.display = 'none';
|
|
||||||
priceInput.removeAttribute('required');
|
|
||||||
});
|
|
||||||
paidBtn.addEventListener('click', () => {
|
|
||||||
paidBtn.classList.add('active');
|
|
||||||
freeBtn.classList.remove('active');
|
|
||||||
priceField.style.display = 'block';
|
|
||||||
priceInput.setAttribute('required', 'required');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Image preview for example image
|
|
||||||
const imageInput = document.getElementById('exampleImage');
|
|
||||||
const imagePreview = document.getElementById('imagePreview');
|
|
||||||
const previewImg = document.getElementById('previewImg');
|
|
||||||
|
|
||||||
if (imageInput) {
|
|
||||||
imageInput.addEventListener('change', function(event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file && (file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg')) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
previewImg.src = e.target.result;
|
|
||||||
imagePreview.style.display = 'block';
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
} else {
|
|
||||||
imagePreview.style.display = 'none';
|
|
||||||
previewImg.src = '#';
|
|
||||||
if (file) alert('Please upload a PNG or JPG image.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle form submission (demo only)
|
|
||||||
document.getElementById('createPromptForm').addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
alert('Prompt published! (Demo)');
|
|
||||||
// Here you would normally send data to a backend (including the image file)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel button (go back)
|
|
||||||
document.querySelector('.cancel-btn').addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch sidebar and topbar
|
|
||||||
fetch('../html/sidebar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
|
||||||
// Remove active class from all sidebar links
|
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
|
||||||
// Optionally set active on "Create New" if it exists, otherwise keep none
|
|
||||||
const createLink = document.querySelector('#sidebar-container a[href="create.html"]');
|
|
||||||
if (createLink) createLink.classList.add('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('../html/topbar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
This file contains global base styles and resets
|
|
||||||
--> ensures consistent spacing, font usage, and box sizing
|
|
||||||
across all elements in the application
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Reset default browser styles */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global body styling */
|
|
||||||
body {
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form errors */
|
|
||||||
.form-error {
|
|
||||||
color: red;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-error ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
list-style: '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-error li {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-error li .error {
|
|
||||||
color: red;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
/* Chats page - Two column layout: chat list + active chat window */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chats-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px 32px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat container (flex, two columns) */
|
|
||||||
.chat-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
overflow: hidden;
|
|
||||||
height: calc(100vh - 120px); /* Adjust based on topbar height */
|
|
||||||
min-height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LEFT COLUMN: Chat list */
|
|
||||||
.chat-list {
|
|
||||||
width: 320px;
|
|
||||||
border-right: 1px solid #eef2f7;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-list-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
.chat-list-header h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.new-chat-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #3b82f6;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-list-items {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
border-bottom: 1px solid #f0f2f5;
|
|
||||||
}
|
|
||||||
.chat-item:hover {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
.chat-item.active {
|
|
||||||
background: #eef2ff;
|
|
||||||
border-left: 3px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-item-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.chat-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.chat-last-msg {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #64748b;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.chat-time {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RIGHT COLUMN: Active chat */
|
|
||||||
.chat-active {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
.chat-avatar-large {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.chat-header-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.chat-header-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.chat-header-status {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.online-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: #10b981;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat messages area */
|
|
||||||
.chat-messages {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
background: #fafcff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 70%;
|
|
||||||
}
|
|
||||||
.message.received {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.message.sent {
|
|
||||||
align-items: flex-end;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.message-bubble {
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 18px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
.message.sent .message-bubble {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.message-time {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input area */
|
|
||||||
.chat-input-area {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.chat-input-area input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.chat-input-area input:focus {
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
.send-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
border: none;
|
|
||||||
padding: 0 24px;
|
|
||||||
border-radius: 30px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.send-btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.chats-main {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.chat-list {
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.chat-container {
|
|
||||||
flex-direction: column;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.chat-list {
|
|
||||||
width: 100%;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
.chat-active {
|
|
||||||
height: 500px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
/* Creators page - Discover creators, filter buttons, creator cards */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creators-main {
|
|
||||||
background: transparent !important;
|
|
||||||
padding: 20px 32px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.creators-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.creators-header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.creators-header p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter buttons */
|
|
||||||
.filter-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 30px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.filter-btn.active {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.filter-btn:hover:not(.active) {
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Creators grid */
|
|
||||||
.creators-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Creator card */
|
|
||||||
.creator-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.creator-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-avatar {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.creator-name {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.creator-handle {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.creator-bio {
|
|
||||||
color: #334155;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.creator-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
.creator-stats i {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
.follow-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.follow-btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.creators-main {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
.filter-buttons {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.creators-main {
|
|
||||||
padding: 12px !important;
|
|
||||||
}
|
|
||||||
.creator-card {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.creator-stats {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.follow-btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
/* Create page - Form for publishing new AI prompts */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px 32px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-container {
|
|
||||||
max-width: 800px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
padding: 32px;
|
|
||||||
transition: box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.create-container:hover {
|
|
||||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.create-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
.create-header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.create-header p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form */
|
|
||||||
.create-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.form-group input,
|
|
||||||
.form-group textarea,
|
|
||||||
.form-group select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid #dbe2ea;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-family: inherit;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
.form-group input:focus,
|
|
||||||
.form-group textarea:focus,
|
|
||||||
.form-group select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #7c3aed;
|
|
||||||
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
|
|
||||||
}
|
|
||||||
.form-hint {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pricing toggle */
|
|
||||||
.pricing-group .pricing-toggle {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.price-option {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 20px;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
.price-option.active {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
#priceField {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.submit-btn, .cancel-btn {
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.submit-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.cancel-btn {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
.submit-btn:hover, .cancel-btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.create-container {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.create-header h1 {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.create-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
/* Feed page - Multi-column grid, square images, like/comment/save actions */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-main {
|
|
||||||
background: transparent !important;
|
|
||||||
padding: 20px 32px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Feed Header (centered) */
|
|
||||||
.feed-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.feed-header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.feed-header p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter Buttons (centered) */
|
|
||||||
.filter-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 30px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.filter-btn.active {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.filter-btn:hover:not(.active) {
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Posts Grid - multi‑column like marketplace */
|
|
||||||
.posts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Post Card */
|
|
||||||
.post-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.post-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Post Header */
|
|
||||||
.post-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #f0f2f5;
|
|
||||||
}
|
|
||||||
.post-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.post-author {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.post-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.post-handle {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
.post-date {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Post Content */
|
|
||||||
.post-content {
|
|
||||||
padding: 12px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.post-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
}
|
|
||||||
.post-description {
|
|
||||||
color: #334155;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
/* Square image */
|
|
||||||
.post-image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Post Actions */
|
|
||||||
.post-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 10px 12px 12px 12px;
|
|
||||||
border-top: 1px solid #f0f2f5;
|
|
||||||
}
|
|
||||||
.action-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.action-btn i {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.like-btn:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
.comment-btn:hover {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
.share-btn:hover {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
.save-btn:hover {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive: single column on small screens */
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.posts-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.feed-main {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
.filter-buttons {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.feed-main {
|
|
||||||
padding: 12px !important;
|
|
||||||
}
|
|
||||||
.post-header {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.post-content {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.post-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.post-actions {
|
|
||||||
padding: 8px 10px 10px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
/*
|
|
||||||
File contains the styles for the login page
|
|
||||||
--> defines the layout of the login screen
|
|
||||||
*/
|
|
||||||
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex; /* enables flexbox layout for centering */
|
|
||||||
justify-content: center; /* horizontally centers the card */
|
|
||||||
align-items: center; /* vertically centers the card */
|
|
||||||
padding: 24px; /* space inside the page edges */
|
|
||||||
/* Layered background: two soft radial gradients for color accents, then the main background color */
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.12), transparent 35%),
|
|
||||||
radial-gradient(circle at bottom right, rgba(236, 72, 153, 0.10), transparent 30%),
|
|
||||||
var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main login card container */
|
|
||||||
.login-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 430px; /* prevents the card from getting too wide on large screens */
|
|
||||||
background: var(--card); /* uses card color from variables.css */
|
|
||||||
border-radius: 24px; /* rounded corners */
|
|
||||||
box-shadow: var(--shadow); /* soft shadow for card elevation */
|
|
||||||
padding: 40px 32px; /* inner spacing for content */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo area above the form */
|
|
||||||
.login-logo-wrapper {
|
|
||||||
display: flex; /* centers logo horizontally */
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden; /* hides any part of the logo that overflows the container */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Full logo styling */
|
|
||||||
.login-logo {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 220px; /* logo never exceeds this width */
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
object-fit: contain; /* keeps logo aspect ratio, prevents stretching */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form layout */
|
|
||||||
.login-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column; /* stack form fields vertically */
|
|
||||||
gap: 18px; /* vertical space between form groups */
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column; /* label above input */
|
|
||||||
gap: 8px; /* space between label and input */
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 16px; /* vertical and horizontal padding for input */
|
|
||||||
border: 1px solid #dbe2ea; /* subtle border */
|
|
||||||
border-radius: 14px; /* rounded input corners */
|
|
||||||
background: #ffffff;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight input when focused */
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none; /* removes default browser outline */
|
|
||||||
border-color: #7c3aed; /* purple border on focus */
|
|
||||||
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.10); /* soft glow for focus state */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Password field with button inside the same row */
|
|
||||||
.password-wrapper {
|
|
||||||
display: flex; /* input and button in one row */
|
|
||||||
align-items: center; /* vertically center input and button */
|
|
||||||
gap: 10px; /* space between input and show/hide button */
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-wrapper input {
|
|
||||||
flex: 1; /* input takes all available space, button stays compact */
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-password {
|
|
||||||
border: none;
|
|
||||||
background: transparent; /* no background for button */
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer; /* pointer cursor for better UX */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main login action button */
|
|
||||||
.login-button {
|
|
||||||
margin-top: 4px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
background: var(--gradient); /* uses a gradient background from variables */
|
|
||||||
color: white;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.22); /* blue shadow for button depth */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
opacity: 0.95; /* slight fade on hover for feedback */
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-text {
|
|
||||||
margin-top: 24px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
/* used for the 'Don't have an account?' and link below the form */
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-text a {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
/* blue link for sign up/login */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smaller spacing and sizing for narrow screens */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
/* Responsive adjustments for small screens (mobile) */
|
|
||||||
.login-card {
|
|
||||||
padding: 28px 20px; /* less padding on mobile */
|
|
||||||
border-radius: 20px; /* slightly less rounded */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: 1.7rem; /* smaller title on mobile */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-logo {
|
|
||||||
max-width: 170px; /* smaller logo on mobile */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,225 +0,0 @@
|
|||||||
/* Marketplace Page - Prompt cards, filter buttons, full width layout */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marketplace-main {
|
|
||||||
background: transparent !important;
|
|
||||||
padding: 20px 32px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header centering */
|
|
||||||
.marketplace-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.marketplace-header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.marketplace-header p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter + Sort Row */
|
|
||||||
.filter-sort-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter buttons - centered */
|
|
||||||
.filter-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 30px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.filter-btn.active {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.filter-btn:hover:not(.active) {
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sort Dropdown - right aligned */
|
|
||||||
.sort-dropdown {
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 30px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #334155;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompts grid */
|
|
||||||
.prompts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompt card */
|
|
||||||
.prompt-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.prompt-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-info {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-author {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-description {
|
|
||||||
color: #334155;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin: 0;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-rating {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
.prompt-rating span:first-child i {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
.prompt-rating span:last-child {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-price {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #3b82f6;
|
|
||||||
margin: 8px 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.buy-btn, .details-btn {
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.buy-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.details-btn {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
.buy-btn:hover, .details-btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.filter-sort-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.sort-dropdown {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.marketplace-main {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.marketplace-main {
|
|
||||||
padding: 12px !important;
|
|
||||||
}
|
|
||||||
.prompt-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
/* Post Detail page - Full prompt view, rating, example output, unlock button */
|
|
||||||
|
|
||||||
/* Full width layout */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-detail-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px 32px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-detail-container {
|
|
||||||
max-width: 900px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
padding: 32px;
|
|
||||||
transition: box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.post-detail-container:hover {
|
|
||||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.post-header {
|
|
||||||
margin-bottom: 28px;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
.post-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.post-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.category {
|
|
||||||
background: #f1f5f9;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
.updated {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
.post-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
.post-stats i {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompt Section */
|
|
||||||
.prompt-section {
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
.prompt-section h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
letter-spacing: -0.3px;
|
|
||||||
}
|
|
||||||
.prompt-content {
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
.prompt-content ul {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.prompt-content li {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rating & Like */
|
|
||||||
.rating-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
.rating-stars {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.rating-stars i {
|
|
||||||
color: #f59e0b;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
.like-btn {
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
padding: 8px 18px;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #475569;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.like-btn:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-color: #cbd5e1;
|
|
||||||
}
|
|
||||||
.like-btn i {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Example Output Section */
|
|
||||||
.example-section {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
.example-section h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.example-content {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #eef2f7;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.example-content h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
.example-output-text {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
.example-output-text p {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.example-image {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.example-image img {
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Unlock Section */
|
|
||||||
.unlock-section {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.unlock-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
border: none;
|
|
||||||
padding: 14px 32px;
|
|
||||||
border-radius: 40px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.unlock-btn:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.post-detail-main {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.post-detail-container {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.post-title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.post-detail-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.rating-section {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.prompt-content {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
/* Profile Page - Full width layout, darker share button, responsive grid */
|
|
||||||
|
|
||||||
/* Force main content container to full width, remove centering and max-width */
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inner spacing for the profile card */
|
|
||||||
.profile-main {
|
|
||||||
background: transparent !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 20px 32px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1600px; /* Limits content on very large screens, but still wide */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make prompts grid use more columns on large screens */
|
|
||||||
.profile-main section:last-child {
|
|
||||||
display: grid !important;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
|
||||||
gap: 24px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Share button: darker background and text */
|
|
||||||
.profile-header button:last-child {
|
|
||||||
background: #cbd5e1 !important; /* darker gray */
|
|
||||||
color: #1e293b !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons keep rounded corners */
|
|
||||||
.login-button {
|
|
||||||
border-radius: 14px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompt cards: rounded corners */
|
|
||||||
.profile-main section > div {
|
|
||||||
border-radius: 18px !important;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompt images: rounded corners */
|
|
||||||
.profile-main section img {
|
|
||||||
border-radius: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avatar remains round */
|
|
||||||
.profile-avatar {
|
|
||||||
border-radius: 50% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* All outer containers stay square */
|
|
||||||
.layout,
|
|
||||||
.profile-main,
|
|
||||||
.profile-header,
|
|
||||||
.profile-tabs,
|
|
||||||
nav {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive: tablets */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.profile-main {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
.profile-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start !important;
|
|
||||||
}
|
|
||||||
.profile-header > div:last-child {
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive: mobile */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.profile-main {
|
|
||||||
padding: 12px !important;
|
|
||||||
}
|
|
||||||
.profile-header > div:last-child {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.profile-main section:last-child {
|
|
||||||
grid-template-columns: 1fr !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,184 +0,0 @@
|
|||||||
/* Settings page - tabs, form styling */
|
|
||||||
|
|
||||||
.layout > div[style*="flex:1"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px 32px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-container {
|
|
||||||
max-width: 700px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
|
||||||
padding: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
.settings-header h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.settings-header p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.settings-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
.tab-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.tab-btn.active {
|
|
||||||
color: #3b82f6;
|
|
||||||
border-bottom-color: #3b82f6;
|
|
||||||
}
|
|
||||||
.tab-btn:hover:not(.active) {
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab content */
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form elements */
|
|
||||||
.settings-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.form-group input,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid #dbe2ea;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
.form-group input:focus,
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #7c3aed;
|
|
||||||
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
|
|
||||||
}
|
|
||||||
.checkbox-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-weight: normal;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.checkbox-label input {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avatar upload */
|
|
||||||
.avatar-upload {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.settings-avatar {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.upload-btn {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.upload-btn:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save button */
|
|
||||||
.save-btn {
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.save-btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.settings-container {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.settings-header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.settings-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.settings-tabs {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.tab-btn {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.avatar-upload {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
Sidebar styles for OnlyPrompt
|
|
||||||
- modern soft card look
|
|
||||||
- responsive: full sidebar on desktop, icon-only on smaller screens
|
|
||||||
- logout button appears directly after the last menu item with separator line
|
|
||||||
*/
|
|
||||||
|
|
||||||
.sidebar-shell {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #ffffff;
|
|
||||||
border-right: 1px solid #eef2f7;
|
|
||||||
padding: 24px 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.sidebar-logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 72px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logo-full {
|
|
||||||
max-width: 170px;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logo-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation – normal block layout, no flex grow */
|
|
||||||
.sidebar {
|
|
||||||
/* No flex:1 – keeps navigation at its natural height */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar li {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 12px 16px;
|
|
||||||
transition: background 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar i {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-blue {
|
|
||||||
color: #3b82f6 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-purple {
|
|
||||||
color: #8b5cf6 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-pink {
|
|
||||||
color: #ec4899 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active item */
|
|
||||||
.sidebar a.active {
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #2563eb;
|
|
||||||
border-left: 3px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bottom logout area – directly after the menu, with separator line */
|
|
||||||
.sidebar-bottom {
|
|
||||||
margin-top: 16px; /* Small gap above the separator */
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logout {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logout:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logout i {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-arrow {
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive: icon-only sidebar */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.sidebar-shell {
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logo-full {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-logo-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-text,
|
|
||||||
.sidebar-logout .nav-text,
|
|
||||||
.logout-arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a,
|
|
||||||
.sidebar-logout {
|
|
||||||
justify-content: center;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a.active {
|
|
||||||
border-left: none;
|
|
||||||
border-right: 3px solid #3b82f6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.sidebar a,
|
|
||||||
.sidebar-logout {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
File contains the styles for the signup page
|
|
||||||
--> defines the layout of the signup screen
|
|
||||||
*/
|
|
||||||
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 24px;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.12), transparent 35%),
|
|
||||||
radial-gradient(circle at bottom right, rgba(236, 72, 153, 0.10), transparent 30%),
|
|
||||||
var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main signup card container */
|
|
||||||
.login-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 430px;
|
|
||||||
background: var(--card); /*variable.css*/
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
padding: 40px 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo area above the form */
|
|
||||||
.login-logo-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Full logo styling */
|
|
||||||
.login-logo {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 220px;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form layout */
|
|
||||||
.login-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border: 1px solid #dbe2ea;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #ffffff;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight input when focused */
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #7c3aed;
|
|
||||||
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Password field with button inside the same row */
|
|
||||||
.password-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-wrapper input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-password {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-terms {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin: 18px 0 0 0;
|
|
||||||
}
|
|
||||||
.signup-terms a {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main login action button */
|
|
||||||
.login-button {
|
|
||||||
margin-top: 4px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
background: var(--gradient);
|
|
||||||
color: white;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-text {
|
|
||||||
margin-top: 24px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-text a {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smaller spacing and sizing for narrow screens */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.login-card {
|
|
||||||
padding: 28px 20px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-logo {
|
|
||||||
max-width: 170px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
Topbar styles for OnlyPrompt
|
|
||||||
- clean, modern, full-width
|
|
||||||
- search bar centered (expands on full screen), profile avatar always on the right
|
|
||||||
- ONLY search bar and avatar have rounded corners
|
|
||||||
*/
|
|
||||||
|
|
||||||
.topbar-shell {
|
|
||||||
width: 100%;
|
|
||||||
background: #ffffff;
|
|
||||||
border-bottom: 1px solid #eef2f7;
|
|
||||||
padding: 16px 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-search {
|
|
||||||
flex: 1; /* Takes all available space */
|
|
||||||
max-width: none; /* No upper limit, expands freely */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 14px; /* Rounded like login inputs */
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-search i {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-search input {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-search input::placeholder {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icons and avatar container */
|
|
||||||
.topbar-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-icon-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #475569;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: background 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-icon-btn:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-avatar-btn {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 50%; /* Avatar round */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.topbar-shell {
|
|
||||||
padding: 12px 20px;
|
|
||||||
}
|
|
||||||
.topbar-search {
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
.topbar-search i {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.topbar-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
.topbar-icon-btn {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
.topbar-actions {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.topbar-shell {
|
|
||||||
padding: 10px 16px;
|
|
||||||
}
|
|
||||||
.topbar-search {
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
This file contains global design variables such as colors,
|
|
||||||
gradients, spacing, and shadows
|
|
||||||
--> these variables ensure a consistent design accross the whole application
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Main gradient used for buttons and highlights */
|
|
||||||
--gradient: linear-gradient(90deg, #7c3aed, #3b82f6, #ec4899);
|
|
||||||
|
|
||||||
/* Background color of the application */
|
|
||||||
--bg: #f8fafc;
|
|
||||||
|
|
||||||
/* Default text color */
|
|
||||||
--text: #0f172a;
|
|
||||||
|
|
||||||
/* Container background */
|
|
||||||
--card: #ffffff;
|
|
||||||
|
|
||||||
/* Border radius for rounded elements */
|
|
||||||
--radius: 16px;
|
|
||||||
|
|
||||||
/* Standard shadow for cards and components */
|
|
||||||
--shadow: 0 10px 30px rgba(0,0,0,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
<!-- OnlyPrompt - Feed page:
|
|
||||||
- Social media style post feed with likes, comments, saves, and share actions (following/foryou tabs) -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>OnlyPrompt - Feed</title>
|
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
|
||||||
<link rel="stylesheet" href="../css/dashboard.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
|
||||||
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
|
||||||
|
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="feed-main">
|
|
||||||
|
|
||||||
<!-- Optional: Feed Header -->
|
|
||||||
<div class="feed-header">
|
|
||||||
<h1>Feed</h1>
|
|
||||||
<p>Latest prompts and inspiration from creators you follow</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter Buttons (optional) -->
|
|
||||||
<div class="filter-buttons">
|
|
||||||
<button class="filter-btn active">For You</button>
|
|
||||||
<button class="filter-btn">Following</button>
|
|
||||||
<button class="filter-btn">Trending</button>
|
|
||||||
<button class="filter-btn">Recent</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Posts Grid (einfach als Liste / Grid – hier als Grid wie Marketplace) -->
|
|
||||||
<div class="posts-grid">
|
|
||||||
|
|
||||||
<!-- Post 1 -->
|
|
||||||
<div class="post-card" onclick="location.href='post-detail.html?id=1'">
|
|
||||||
<div class="post-header">
|
|
||||||
<img src="../images/content/creator1.png" alt="Sarah Jenkins" class="post-avatar">
|
|
||||||
<div class="post-author">
|
|
||||||
<span class="post-name">Sarah Jenkins</span>
|
|
||||||
<span class="post-handle">@sarahj</span>
|
|
||||||
</div>
|
|
||||||
<span class="post-date">2 hours ago</span>
|
|
||||||
</div>
|
|
||||||
<div class="post-content">
|
|
||||||
<h3 class="post-title">Conceptual Landscape Art</h3>
|
|
||||||
<p class="post-description">Enchanting, vintage, antique vibes. A journey through surreal landscapes.</p>
|
|
||||||
<img src="../images/content/feed1.png" alt="Conceptual Landscape" class="post-image">
|
|
||||||
</div>
|
|
||||||
<!-- Like, Comment, Share, Save Buttons -->
|
|
||||||
<div class="post-actions">
|
|
||||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
|
||||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
|
||||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
|
||||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Post 2 -->
|
|
||||||
<div class="post-card" onclick="location.href='post-detail.html?id=2'">
|
|
||||||
<div class="post-header">
|
|
||||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="post-avatar">
|
|
||||||
<div class="post-author">
|
|
||||||
<span class="post-name">Alex Chen</span>
|
|
||||||
<span class="post-handle">@alexchen</span>
|
|
||||||
</div>
|
|
||||||
<span class="post-date">Yesterday</span>
|
|
||||||
</div>
|
|
||||||
<div class="post-content">
|
|
||||||
<h3 class="post-title">Minimalist Logo Design</h3>
|
|
||||||
<p class="post-description">Clean, modern, minimalist logo for tech startups.</p>
|
|
||||||
<img src="../images/content/feed2.png" alt="Minimalist Logo" class="post-image">
|
|
||||||
</div>
|
|
||||||
<!-- Like, Comment, Share, Save Buttons -->
|
|
||||||
<div class="post-actions">
|
|
||||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
|
||||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
|
||||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
|
||||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Post 3 -->
|
|
||||||
<div class="post-card" onclick="location.href='post-detail.html?id=3'">
|
|
||||||
<div class="post-header">
|
|
||||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="post-avatar">
|
|
||||||
<div class="post-author">
|
|
||||||
<span class="post-name">Mia Wong</span>
|
|
||||||
<span class="post-handle">@miawong</span>
|
|
||||||
</div>
|
|
||||||
<span class="post-date">3 days ago</span>
|
|
||||||
</div>
|
|
||||||
<div class="post-content">
|
|
||||||
<h3 class="post-title">Futuristic Cityscape</h3>
|
|
||||||
<p class="post-description">Cyberpunk neon city with flying cars and rain.</p>
|
|
||||||
<img src="../images/content/feed3.png" alt="Cityscape" class="post-image">
|
|
||||||
</div>
|
|
||||||
<!-- Like, Comment, Share, Save Buttons -->
|
|
||||||
<div class="post-actions">
|
|
||||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
|
||||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
|
||||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
|
||||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Post 4 -->
|
|
||||||
<div class="post-card" onclick="location.href='post-detail.html?id=4'">
|
|
||||||
<div class="post-header">
|
|
||||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="post-avatar">
|
|
||||||
<div class="post-author">
|
|
||||||
<span class="post-name">Tom Rivera</span>
|
|
||||||
<span class="post-handle">@tomrivera</span>
|
|
||||||
</div>
|
|
||||||
<span class="post-date">5 days ago</span>
|
|
||||||
</div>
|
|
||||||
<div class="post-content">
|
|
||||||
<h3 class="post-title">Watercolor Pet Portrait</h3>
|
|
||||||
<p class="post-description">Soft watercolor style, cute pet portrait.</p>
|
|
||||||
<img src="../images/content/feed4.png" alt="Watercolor Pet" class="post-image">
|
|
||||||
</div>
|
|
||||||
<!-- Like, Comment, Share, Save Buttons -->
|
|
||||||
<div class="post-actions">
|
|
||||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
|
||||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
|
||||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
|
||||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
fetch('../html/sidebar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
|
||||||
// Remove 'active' from all sidebar links
|
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
|
||||||
// Set 'active' on the first link (Dashboard) - index 0
|
|
||||||
const firstLink = document.querySelectorAll('#sidebar-container .sidebar li a')[0];
|
|
||||||
if (firstLink) firstLink.classList.add('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('../html/topbar.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 660 KiB |
|
Before Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 868 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 63 KiB |
@ -1,150 +0,0 @@
|
|||||||
// LINQ-like Enumerable class wrapping lazy generator chains
|
|
||||||
|
|
||||||
class Enumerable {
|
|
||||||
constructor(iteratorFn) {
|
|
||||||
this._iteratorFn = iteratorFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return this._iteratorFn();
|
|
||||||
}
|
|
||||||
|
|
||||||
_chain(generatorFn) {
|
|
||||||
const source = this;
|
|
||||||
return new Enumerable(function* () {
|
|
||||||
yield* generatorFn(source);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
where(predicate) {
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
for (const item of source) if (predicate(item)) yield item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
select(selector) {
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
for (const item of source) yield selector(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
take(count) {
|
|
||||||
count = Math.max(0, count);
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
for (const item of source) {
|
|
||||||
if (count-- <= 0) break;
|
|
||||||
yield item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
skip(count) {
|
|
||||||
count = Math.max(0, count);
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
for (const item of source) {
|
|
||||||
if (count-- > 0) continue;
|
|
||||||
yield item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isLast() {
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
const iter = source[Symbol.iterator]();
|
|
||||||
let current = iter.next();
|
|
||||||
let index = 0;
|
|
||||||
while (!current.done) {
|
|
||||||
const next = iter.next();
|
|
||||||
yield [current.value, next.done, index];
|
|
||||||
current = next;
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(action) {
|
|
||||||
for (const item of this) {
|
|
||||||
if (Array.isArray(item)) {
|
|
||||||
action(...item);
|
|
||||||
} else {
|
|
||||||
action(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toArray() {
|
|
||||||
return Array.from(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
orderByDescending(keySelector) {
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
const items = Array.from(source);
|
|
||||||
items.sort((a, b) => keySelector(b) - keySelector(a));
|
|
||||||
yield* items;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
orderBy(keySelector) {
|
|
||||||
return this._chain(function* (source) {
|
|
||||||
const items = Array.from(source);
|
|
||||||
items.sort((a, b) => keySelector(a) - keySelector(b));
|
|
||||||
yield* items;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
firstOrDefault(predicate) {
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
for (const item of source) return item;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
first(predicate) {
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
for (const item of source) return item;
|
|
||||||
throw new Error("No elements in sequence.");
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOrDefault(predicate) {
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
let lastValue = undefined;
|
|
||||||
for (const item of source) lastValue = item;
|
|
||||||
return lastValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
last(predicate) {
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
let lastValue = undefined;
|
|
||||||
let found = false;
|
|
||||||
for (const item of source) {
|
|
||||||
lastValue = item;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
if (!found) throw new Error("No elements in sequence.");
|
|
||||||
return lastValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
any(predicate) {
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
for (const _ of source) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
all(predicate) {
|
|
||||||
for (const item of this) if (!predicate(item)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
count(predicate) {
|
|
||||||
let count = 0;
|
|
||||||
const source = predicate ? this.where(predicate) : this;
|
|
||||||
for (const _ of source) count++;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.prototype.asEnumerable = function () {
|
|
||||||
const arr = this;
|
|
||||||
return new Enumerable(function* () {
|
|
||||||
for (const item of arr) yield item;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { sendFormAsync } from "./shared.js";
|
|
||||||
|
|
||||||
function togglePassword() {
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const newInputType = passwordInput.type === 'password' ? 'text' : 'password';
|
|
||||||
passwordInput.type = newInputType;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitLoginForm(){
|
|
||||||
const form = document.getElementById('loginForm');
|
|
||||||
await sendFormAsync(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const togglePasswordButton = document.getElementById('togglePassword');
|
|
||||||
togglePasswordButton.addEventListener('click', togglePassword);
|
|
||||||
const loginForm = document.getElementById('loginForm');
|
|
||||||
loginForm.addEventListener('submit', async (event) => {
|
|
||||||
event.preventDefault(); // Prevent the default form submission
|
|
||||||
await submitLoginForm();
|
|
||||||
});
|
|
||||||
@ -1,556 +0,0 @@
|
|||||||
import "./linq.js";
|
|
||||||
import { NodeTemplate } from "./node-template.js";
|
|
||||||
|
|
||||||
// ─── Route matching ───
|
|
||||||
|
|
||||||
class RouteMatch {
|
|
||||||
constructor(route, path, params) {
|
|
||||||
this.route = route;
|
|
||||||
this.path = path;
|
|
||||||
this.params = params;
|
|
||||||
this.segmentCount = route.fragments.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Route {
|
|
||||||
constructor(pattern, componentClass) {
|
|
||||||
this.id = crypto.randomUUID();
|
|
||||||
this.componentClass = componentClass;
|
|
||||||
this.componentDefinition = componentClass.definition;
|
|
||||||
this.fragments = pattern.split("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
match(path) {
|
|
||||||
const parsedUrl = new URL(path, window.location.origin);
|
|
||||||
const params = {};
|
|
||||||
parsedUrl.searchParams.forEach((value, key) => {
|
|
||||||
params[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pathFragments = parsedUrl.pathname.split("/");
|
|
||||||
|
|
||||||
for (let i = 0; i < this.fragments.length; i++) {
|
|
||||||
const fragment = this.fragments[i];
|
|
||||||
const pathFragment = pathFragments[i];
|
|
||||||
if (fragment.startsWith(":")) {
|
|
||||||
params[fragment.substring(1)] = pathFragment;
|
|
||||||
continue;
|
|
||||||
} else if (fragment === "*") {
|
|
||||||
continue;
|
|
||||||
} else if (fragment === "**") {
|
|
||||||
return new RouteMatch(this, path, params);
|
|
||||||
} else if (fragment !== pathFragment) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RouteMatch(this, path, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ComponentInput (signal-like) ───
|
|
||||||
|
|
||||||
export class ComponentInput {
|
|
||||||
constructor(defaultValue, options = {}) {
|
|
||||||
this._value = defaultValue;
|
|
||||||
this.transform = options.transform || ((v) => v);
|
|
||||||
this.validate = options.validate || (() => true);
|
|
||||||
this.alias = options.alias || null;
|
|
||||||
this._isComponentInput = true;
|
|
||||||
this._owner = null;
|
|
||||||
const self = this;
|
|
||||||
const fn = function () {
|
|
||||||
return self._value;
|
|
||||||
};
|
|
||||||
return new Proxy(fn, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop === "set") return (v) => self._set(v);
|
|
||||||
if (prop === "_isComponentInput") return true;
|
|
||||||
if (prop === "_self") return self;
|
|
||||||
if (prop === "alias") return self.alias;
|
|
||||||
if (prop === Symbol.toPrimitive) return () => self._value;
|
|
||||||
if (prop === "valueOf") return () => self._value;
|
|
||||||
if (prop === "toString") return () => String(self._value);
|
|
||||||
const inner = self._value;
|
|
||||||
if (inner == null) return undefined;
|
|
||||||
const val = inner[prop];
|
|
||||||
return typeof val === "function" ? val.bind(inner) : val;
|
|
||||||
},
|
|
||||||
set(target, prop, value) {
|
|
||||||
if (self._value != null) {
|
|
||||||
self._value[prop] = value;
|
|
||||||
if (self._owner) self._owner.requestUpdate();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
apply(target, thisArg, args) {
|
|
||||||
return self._value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_set(newValue) {
|
|
||||||
const transformed = this.transform(newValue);
|
|
||||||
if (!this.validate(transformed)) {
|
|
||||||
throw new Error("Invalid value");
|
|
||||||
}
|
|
||||||
this._value = transformed;
|
|
||||||
if (this._owner) this._owner.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
static [Symbol.hasInstance](instance) {
|
|
||||||
return instance?._isComponentInput === true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ViewChild ───
|
|
||||||
|
|
||||||
export class ViewChild {
|
|
||||||
constructor(options = { selector: null, id: null, multiple: false }) {
|
|
||||||
this._selector = options.selector;
|
|
||||||
this._id = options.id;
|
|
||||||
this._multiple = options.multiple;
|
|
||||||
this._element = null;
|
|
||||||
this._isViewChild = true;
|
|
||||||
return new Proxy(this, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop in target) return target[prop];
|
|
||||||
if (target._element) {
|
|
||||||
const value = target._element[prop];
|
|
||||||
return typeof value === "function"
|
|
||||||
? value.bind(target._element)
|
|
||||||
: value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setValue(element) {
|
|
||||||
this._element = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
static [Symbol.hasInstance](instance) {
|
|
||||||
return instance?._isViewChild === true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ComponentDefinition ───
|
|
||||||
|
|
||||||
export class ComponentDefinition {
|
|
||||||
constructor(
|
|
||||||
options = {
|
|
||||||
templatesPath: null,
|
|
||||||
template: null,
|
|
||||||
stylesPath: null,
|
|
||||||
style: null,
|
|
||||||
scriptsPath: null,
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
this.templatesPath = options.templatesPath;
|
|
||||||
this.template = options.template;
|
|
||||||
this.stylesPath = options.stylesPath;
|
|
||||||
this.style = options.style;
|
|
||||||
this.scriptsPath = options.scriptsPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── EventListener ───
|
|
||||||
|
|
||||||
class EventBinding {
|
|
||||||
constructor(event, element, handler) {
|
|
||||||
this.event = event;
|
|
||||||
this.element = element;
|
|
||||||
this.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
attach() {
|
|
||||||
this.element.addEventListener(this.event, this.handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
detach() {
|
|
||||||
this.element.removeEventListener(this.event, this.handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Component ───
|
|
||||||
|
|
||||||
export class Component extends EventTarget {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._eventBindings = [];
|
|
||||||
return new Proxy(this, {
|
|
||||||
get(target, prop, receiver) {
|
|
||||||
const value = Reflect.get(target, prop, target);
|
|
||||||
if (typeof value === "function" && typeof value.bind === "function") {
|
|
||||||
return prop in EventTarget.prototype
|
|
||||||
? value.bind(target)
|
|
||||||
: value.bind(receiver);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
set(target, prop, value) {
|
|
||||||
if (target[prop] instanceof ComponentInput) {
|
|
||||||
target[prop].set(value);
|
|
||||||
return true;
|
|
||||||
} else if (target[prop] instanceof ViewChild) {
|
|
||||||
target[prop]._setValue(value);
|
|
||||||
} else {
|
|
||||||
target[prop] = value;
|
|
||||||
}
|
|
||||||
target.requestUpdate();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
requestUpdate() {
|
|
||||||
this.dispatchEvent(new Event("requestUpdate"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static get definition() {
|
|
||||||
throw new Error("Component definition is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
onInit() { }
|
|
||||||
onBeforeRender() { }
|
|
||||||
onAfterRender() { }
|
|
||||||
onDestroy() {
|
|
||||||
this._eventBindings.forEach((b) => b.detach());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Router init ───
|
|
||||||
|
|
||||||
export function initRouter(outletId, routes) {
|
|
||||||
const outletRef = document.getElementById(outletId);
|
|
||||||
if (!outletRef) {
|
|
||||||
console.error(`Outlet element with id '${outletId}' not found`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Router(outletRef, routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Router ───
|
|
||||||
|
|
||||||
export class Router {
|
|
||||||
constructor(outletRef, routes) {
|
|
||||||
this.outletRef = outletRef;
|
|
||||||
this.shadowRoot = outletRef.attachShadow({ mode: "open" });
|
|
||||||
this.routes = routes;
|
|
||||||
this.templateCache = new Map();
|
|
||||||
this.activeController = null;
|
|
||||||
this.activeRenderResult = null;
|
|
||||||
this._boundUpdateHandler = null;
|
|
||||||
this._rendering = false;
|
|
||||||
this._pendingUpdate = false;
|
|
||||||
this.shadowRoot.textContent = "Loading...";
|
|
||||||
navigation.addEventListener("navigate", this.handleNavigate.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
findMatchingRoute(path) {
|
|
||||||
return this.routes
|
|
||||||
.asEnumerable()
|
|
||||||
.select((route) => route.match(path))
|
|
||||||
.where((match) => match !== null)
|
|
||||||
.orderByDescending((match) => match.segmentCount)
|
|
||||||
.firstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleNavigate(event) {
|
|
||||||
if (event.navigationType === 'replace' && event.destination.url === this.activeRoute) return;
|
|
||||||
const routeMatch = this.findMatchingRoute(event.destination.url);
|
|
||||||
if (!routeMatch) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const template = await this.getCachedTemplate(routeMatch.route);
|
|
||||||
const controller = this.createComponentInstance(
|
|
||||||
routeMatch.route.componentClass,
|
|
||||||
routeMatch.params,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.destroyCurrent();
|
|
||||||
this.activeController = controller;
|
|
||||||
this.activeTemplate = template;
|
|
||||||
this.activeRoute = event.destination.url;
|
|
||||||
this.insertIncludes(routeMatch.route);
|
|
||||||
|
|
||||||
this._boundUpdateHandler = this.handleUpdateRequest.bind(this);
|
|
||||||
this.activeController.addEventListener(
|
|
||||||
"requestUpdate",
|
|
||||||
this._boundUpdateHandler,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.renderActiveComponent();
|
|
||||||
history.replaceState({}, "", event.destination.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyCurrent() {
|
|
||||||
if (this.activeController) {
|
|
||||||
this.activeController.removeEventListener(
|
|
||||||
"requestUpdate",
|
|
||||||
this._boundUpdateHandler,
|
|
||||||
);
|
|
||||||
this.activeController.onDestroy();
|
|
||||||
this.activeController = null;
|
|
||||||
}
|
|
||||||
if (this.activeRenderResult) {
|
|
||||||
this.activeRenderResult.destroy();
|
|
||||||
this.activeRenderResult = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUpdateRequest() {
|
|
||||||
if (this._rendering) {
|
|
||||||
this._pendingUpdate = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateActiveComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
insertIncludes(route) {
|
|
||||||
// Remove previous component styles from shadow root
|
|
||||||
this.shadowRoot
|
|
||||||
.querySelectorAll("link[data-component-style], style[data-component-style]")
|
|
||||||
.forEach((l) => l.remove());
|
|
||||||
|
|
||||||
if (route.componentDefinition.style) {
|
|
||||||
const styleEl = document.createElement("style");
|
|
||||||
styleEl.textContent = route.componentDefinition.style;
|
|
||||||
styleEl.setAttribute("data-component-style", "");
|
|
||||||
this.shadowRoot.appendChild(styleEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
const paths = route.componentDefinition.stylesPath;
|
|
||||||
if (!paths) return;
|
|
||||||
const list = Array.isArray(paths) ? paths : [paths];
|
|
||||||
for (const p of list) {
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = "stylesheet";
|
|
||||||
link.href = p;
|
|
||||||
link.setAttribute("data-component-style", "");
|
|
||||||
this.shadowRoot.appendChild(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── First render: build DOM from scratch ───
|
|
||||||
|
|
||||||
renderActiveComponent() {
|
|
||||||
this._rendering = true;
|
|
||||||
this._pendingUpdate = false;
|
|
||||||
|
|
||||||
this.activeController.onBeforeRender();
|
|
||||||
|
|
||||||
const result = this.activeTemplate.render(this.activeController);
|
|
||||||
this.activeRenderResult = result;
|
|
||||||
|
|
||||||
// Clear shadow root content (keep style links), append fragment
|
|
||||||
for (const child of Array.from(this.shadowRoot.childNodes)) {
|
|
||||||
if (
|
|
||||||
child.nodeType !== Node.ELEMENT_NODE ||
|
|
||||||
!child.hasAttribute("data-component-style")
|
|
||||||
) {
|
|
||||||
child.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.shadowRoot.appendChild(result.fragment);
|
|
||||||
|
|
||||||
// Wire events + two-way bindings on the live shadow DOM
|
|
||||||
this.wireEvents(this.activeController, this.shadowRoot);
|
|
||||||
this.wireViewChildren(this.activeController, this.shadowRoot);
|
|
||||||
|
|
||||||
this.activeController.onAfterRender();
|
|
||||||
this._rendering = false;
|
|
||||||
|
|
||||||
if (this._pendingUpdate) {
|
|
||||||
this.updateActiveComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Subsequent updates: patch bindings only ───
|
|
||||||
|
|
||||||
updateActiveComponent() {
|
|
||||||
this._rendering = true;
|
|
||||||
this._pendingUpdate = false;
|
|
||||||
|
|
||||||
this.activeController.onBeforeRender();
|
|
||||||
this.activeRenderResult.update(this.activeController);
|
|
||||||
this.activeController.onAfterRender();
|
|
||||||
|
|
||||||
this._rendering = false;
|
|
||||||
|
|
||||||
if (this._pendingUpdate) {
|
|
||||||
this.updateActiveComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ViewChild wiring ───
|
|
||||||
|
|
||||||
wireViewChildren(controller, root) {
|
|
||||||
Object.entries(controller)
|
|
||||||
.filter(([_, value]) => value instanceof ViewChild)
|
|
||||||
.forEach(([key, viewChild]) => {
|
|
||||||
if (viewChild._id) {
|
|
||||||
const el = root.querySelector(`#${viewChild._id}`);
|
|
||||||
if (el) viewChild._setValue(el);
|
|
||||||
} else if (viewChild._selector) {
|
|
||||||
const el = root.querySelector(viewChild._selector);
|
|
||||||
if (el) viewChild._setValue(el);
|
|
||||||
} else {
|
|
||||||
console.warn(`ViewChild ${key}: no selector or id`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Event wiring ───
|
|
||||||
// Supports:
|
|
||||||
// (click)="methodName()" — event binding
|
|
||||||
// [(value)]="propName" — two-way binding (banana-in-a-box)
|
|
||||||
|
|
||||||
wireEvents(controller, root) {
|
|
||||||
const allElements = root.querySelectorAll("*");
|
|
||||||
|
|
||||||
allElements.forEach((element) => {
|
|
||||||
// ─── Two-way bindings from __templateMeta ───
|
|
||||||
if (element.__templateMeta) {
|
|
||||||
for (const meta of element.__templateMeta) {
|
|
||||||
if (meta.bracket === "[()]" && meta.expression) {
|
|
||||||
const domProp = meta.name.replace(/^\(|\)$/g, ""); // strip parens
|
|
||||||
this._bindTwoWay(controller, element, domProp, meta.expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Event bindings from DOM attributes ───
|
|
||||||
for (const attr of Array.from(element.attributes)) {
|
|
||||||
const name = attr.name;
|
|
||||||
|
|
||||||
const eventMatch = name.match(/^\((\w+)\)$/);
|
|
||||||
if (eventMatch) {
|
|
||||||
const eventName = eventMatch[1];
|
|
||||||
const attrValue = attr.value.trim();
|
|
||||||
const methodName = attrValue.endsWith("()")
|
|
||||||
? attrValue.slice(0, -2)
|
|
||||||
: attrValue;
|
|
||||||
|
|
||||||
let binding;
|
|
||||||
if (typeof controller[methodName] === "function") {
|
|
||||||
const handler = controller[methodName].bind(controller);
|
|
||||||
binding = new EventBinding(eventName, element, handler);
|
|
||||||
} else {
|
|
||||||
const fn = new Function("event", attrValue).bind(controller);
|
|
||||||
binding = new EventBinding(eventName, element, fn);
|
|
||||||
}
|
|
||||||
binding.attach();
|
|
||||||
controller._eventBindings.push(binding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Two-way bind: DOM property ↔ controller field ───
|
|
||||||
// Sets DOM prop from controller on each update.
|
|
||||||
// Listens for input/change events to write back to controller.
|
|
||||||
|
|
||||||
_bindTwoWay(controller, element, domProp, field) {
|
|
||||||
// Controller → DOM: set initial value
|
|
||||||
const getValue = () => {
|
|
||||||
const v = controller[field];
|
|
||||||
return typeof v === "function" ? v() : v;
|
|
||||||
};
|
|
||||||
|
|
||||||
element[domProp] = getValue() ?? "";
|
|
||||||
|
|
||||||
// DOM → Controller: on input (for text) + change (for select/checkbox)
|
|
||||||
const writeBack = () => {
|
|
||||||
const domValue = element[domProp];
|
|
||||||
if (controller[field] instanceof ComponentInput) {
|
|
||||||
controller[field].set(domValue);
|
|
||||||
} else {
|
|
||||||
controller[field] = domValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputBinding = new EventBinding("input", element, writeBack);
|
|
||||||
const changeBinding = new EventBinding("change", element, writeBack);
|
|
||||||
inputBinding.attach();
|
|
||||||
changeBinding.attach();
|
|
||||||
controller._eventBindings.push(inputBinding, changeBinding);
|
|
||||||
|
|
||||||
// Controller → DOM: patch on every update via RenderResult hook
|
|
||||||
// We piggyback on requestUpdate listener — the attr binding in
|
|
||||||
// node-template handles {{expr}} attrs, but for [(prop)] we need
|
|
||||||
// to sync the DOM *property* (not attribute) on update.
|
|
||||||
const updateBinding = new EventBinding("requestUpdate", controller, () => {
|
|
||||||
const val = getValue() ?? "";
|
|
||||||
if (element[domProp] !== val) element[domProp] = val;
|
|
||||||
});
|
|
||||||
updateBinding.attach();
|
|
||||||
controller._eventBindings.push(updateBinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Component instance creation ───
|
|
||||||
|
|
||||||
createComponentInstance(component, params) {
|
|
||||||
const instance = new component();
|
|
||||||
|
|
||||||
// Wire _owner for ComponentInput reactivity
|
|
||||||
for (const key of Object.keys(instance)) {
|
|
||||||
const val = instance[key];
|
|
||||||
if (val instanceof ComponentInput) {
|
|
||||||
val._self._owner = instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.onInit();
|
|
||||||
|
|
||||||
// Bind route/query params → ComponentInputs (by name or alias)
|
|
||||||
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
||||||
if (instance[paramKey] instanceof ComponentInput) {
|
|
||||||
instance[paramKey].set(paramValue);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(instance)) {
|
|
||||||
const val = instance[key];
|
|
||||||
if (val instanceof ComponentInput && val.alias === paramKey) {
|
|
||||||
val.set(paramValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Template caching ───
|
|
||||||
|
|
||||||
async getCachedTemplate(route) {
|
|
||||||
if (this.templateCache.has(route.id)) {
|
|
||||||
return this.templateCache.get(route.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let templateContent = route.componentClass.definition.template;
|
|
||||||
if (!templateContent && route.componentClass.definition.templatesPath) {
|
|
||||||
const response = await fetch(
|
|
||||||
route.componentClass.definition.templatesPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return new NodeTemplate(
|
|
||||||
`<div class="error"><h1>Failed to load template</h1><p>${response.status} ${response.statusText}</p></div>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
templateContent = await response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const template = new NodeTemplate(templateContent);
|
|
||||||
this.templateCache.set(route.id, template);
|
|
||||||
return template;
|
|
||||||
} catch (error) {
|
|
||||||
return new NodeTemplate(
|
|
||||||
`<div class="error"><h1>Failed to compile template</h1><p>${error.message}</p></div>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||