feat: add WordPress as runtime — Dockerfile, K8s env vars, wp-content PVC, frontend UI

Backend:
- Add WORDPRESS to AppRuntime enum
- Add wordpressDockerfile() using wordpress:6-php8.3-apache base image
  with custom theme/plugin/wp-content merge support
- Add WordPress-specific K8s env vars (WORDPRESS_DB_HOST, WORDPRESS_DB_USER,
  WORDPRESS_DB_PASSWORD, WORDPRESS_DB_NAME, WORDPRESS_TABLE_PREFIX)
- Create wp-content PersistentVolumeClaim (2Gi) for WordPress deployments
- Mount wp-content PVC in deployment container at /var/www/html/wp-content

Frontend:
- Add 'wordpress' to runtime type unions (Application, CreateApplicationDto)
- Add WordPress runtime card in deploy page (port 80, blue icon)
- Auto-select MySQL database when WordPress is chosen, disable other DB options
- Show Persian hint 'وردپرس به MySQL نیاز دارد' when WordPress selected
- Update all runtime icon colors across dashboard, apps, admin/apps pages
  to show blue-600 for WordPress
This commit is contained in:
keyhan
2026-04-06 23:13:39 +03:30
parent ea86acf2ab
commit c411c5873e
10 changed files with 134 additions and 15 deletions
@@ -190,7 +190,7 @@ export default function AdminAppsPage() {
<td className="px-6 py-4">
<Link href={`/dashboard/apps/${app.id}`} className="flex items-center gap-3 group">
<div className="w-9 h-9 rounded-lg bg-primary-50 flex items-center justify-center shrink-0">
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : 'text-orange-500'}`} />
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<div>
<span className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors block">
@@ -254,7 +254,7 @@ export default function AdminAppsPage() {
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary-50 flex items-center justify-center">
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : 'text-orange-500'}`} />
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<div>
<h3 className="font-semibold text-gray-900">{app.name}</h3>
@@ -358,7 +358,7 @@ export default function AppDetailPage() {
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="w-14 h-14 rounded-2xl bg-primary-50 flex items-center justify-center shrink-0">
<Hexagon className={`w-7 h-7 ${app.runtime === 'nodejs' ? 'text-green-500' : 'text-orange-500'}`} />
<Hexagon className={`w-7 h-7 ${app.runtime === 'nodejs' ? 'text-green-500' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
+2 -2
View File
@@ -101,7 +101,7 @@ export default function AppsPage() {
<td className="px-6 py-4">
<Link href={`/dashboard/apps/${app.id}`} className="flex items-center gap-3 group">
<div className="w-9 h-9 rounded-lg bg-primary-50 flex items-center justify-center shrink-0">
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : 'text-orange-500'}`} />
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<span className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors">
{app.name}
@@ -149,7 +149,7 @@ export default function AppsPage() {
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary-50 flex items-center justify-center">
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : 'text-orange-500'}`} />
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<div>
<h3 className="font-semibold text-gray-900">{app.name}</h3>
+23 -6
View File
@@ -363,15 +363,22 @@ export default function DeployPage() {
<h2 className="text-lg font-semibold text-gray-900">Runtime & Database</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">Application Runtime</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ value: 'nodejs', label: 'Node.js', icon: <Hexagon className="w-6 h-6 text-green-500" />, desc: 'Express, NestJS, Fastify...' },
{ value: 'laravel', label: 'Laravel', icon: <Hexagon className="w-6 h-6 text-orange-500" />, desc: 'PHP 8.3, Composer, Artisan' },
{ value: 'wordpress', label: 'WordPress', icon: <Hexagon className="w-6 h-6 text-blue-600" />, desc: 'PHP 8.3, Apache, wp-content' },
].map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setForm({ ...form, runtime: opt.value as any, port: opt.value === 'nodejs' ? 3000 : 8000 })}
onClick={() => {
const updates: any = { runtime: opt.value as any };
if (opt.value === 'nodejs') updates.port = 3000;
else if (opt.value === 'laravel') updates.port = 8000;
else if (opt.value === 'wordpress') { updates.port = 80; updates.databaseType = 'mysql'; }
setForm({ ...form, ...updates });
}}
className={`p-4 rounded-xl border-2 text-left transition-colors ${
form.runtime === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
}`}
@@ -384,25 +391,35 @@ export default function DeployPage() {
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">Database</label>
<label className="block text-sm font-medium text-gray-700 mb-3">
Database
{form.runtime === 'wordpress' && (
<span className="text-xs text-blue-500 mr-2"> وردپرس به MySQL نیاز دارد</span>
)}
</label>
<div className="grid grid-cols-3 gap-3 sm:gap-4">
{[
{ value: 'none', label: 'None', icon: <XCircle className="w-6 h-6 text-gray-400" /> },
{ value: 'postgresql', label: 'PostgreSQL', icon: <svg className="w-6 h-6 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19C3 20.66 7.03 22 12 22C16.97 22 21 20.66 21 19V5"/><path d="M3 12C3 13.66 7.03 15 12 15C16.97 15 21 13.66 21 12"/></svg> },
{ value: 'mysql', label: 'MySQL', icon: <svg className="w-6 h-6 text-orange-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19C3 20.66 7.03 22 12 22C16.97 22 21 20.66 21 19V5"/><path d="M3 12C3 13.66 7.03 15 12 15C16.97 15 21 13.66 21 12"/></svg> },
].map((opt) => (
].map((opt) => {
const isWordPress = form.runtime === 'wordpress';
const disabled = isWordPress && opt.value !== 'mysql';
return (
<button
key={opt.value}
type="button"
disabled={disabled}
onClick={() => setForm({ ...form, databaseType: opt.value as any })}
className={`p-4 rounded-xl border-2 text-center transition-colors ${
form.databaseType === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
}`}
} ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
>
<div className="flex justify-center">{opt.icon}</div>
<p className="mt-2 font-semibold text-sm text-gray-900">{opt.label}</p>
</button>
))}
);
})}
</div>
</div>
+1 -1
View File
@@ -123,7 +123,7 @@ export default function DashboardPage() {
>
<div className="flex items-center gap-4">
<div className="w-11 h-11 rounded-xl bg-primary-50 flex items-center justify-center shrink-0">
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : 'text-orange-500'}`} />
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
</div>
<div className="min-w-0">
<h3 className="font-semibold text-gray-900 group-hover:text-primary-700 transition-colors truncate">
+2 -2
View File
@@ -13,7 +13,7 @@ export interface Application {
id: string;
name: string;
description?: string;
runtime: 'nodejs' | 'laravel';
runtime: 'nodejs' | 'laravel' | 'wordpress';
databaseType: 'mysql' | 'postgresql' | 'none';
dbUsername?: string;
dbPassword?: string;
@@ -87,7 +87,7 @@ export interface AuthResponse {
export interface CreateApplicationDto {
name: string;
description?: string;
runtime: 'nodejs' | 'laravel';
runtime: 'nodejs' | 'laravel' | 'wordpress';
databaseType: 'mysql' | 'postgresql' | 'none';
dbUsername?: string;
dbPassword?: string;
File diff suppressed because one or more lines are too long