diff --git a/k8s/app.yaml b/k8s/app.yaml
index ad41594..7c5b76b 100644
--- a/k8s/app.yaml
+++ b/k8s/app.yaml
@@ -1,105 +1,89 @@
-apiVersion: v1
-kind: Namespace
-metadata:
- name: ${NAMESPACE}
----
apiVersion: apps/v1
kind: Deployment
metadata:
- name: la-huasca-nextjs
- namespace: ${NAMESPACE}
+ name: la-huasca-ts
+ namespace: la-huasca-ts
spec:
- replicas: 2
- strategy:
- type: RollingUpdate
- rollingUpdate:
- maxSurge: 1
- maxUnavailable: 0
+ replicas: 1
selector:
matchLabels:
- app: la-huasca-nextjs
+ app: la-huasca-ts
template:
metadata:
labels:
- app: la-huasca-nextjs
+ app: la-huasca-ts
spec:
imagePullSecrets:
- name: la-huasca-registry
containers:
- - name: nextjs
- image: gitea.ajavasoft.com/muken/la-huasca-ts:${TAG}
+ - name: la-huasca-ts
+ image: gitea.ajavasoft.com/muken/la-huasca-ts:latest
ports:
- containerPort: 3000
+ name: http
env:
+ - name: NODE_ENV
+ value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
- name: la-huasca-db-secrets
- key: DATABASE_URL
- - name: JWT_SECRET
- valueFrom:
- secretKeyRef:
- name: la-huasca-app-secrets
- key: JWT_SECRET
+ name: calendar-db-secrets
+ key: url
readinessProbe:
httpGet:
- path: /
- port: 3000
- initialDelaySeconds: 5
- periodSeconds: 5
+ path: /api/health
+ port: http
+ initialDelaySeconds: 20
+ periodSeconds: 15
livenessProbe:
httpGet:
- path: /
- port: 3000
- initialDelaySeconds: 15
- periodSeconds: 10
+ path: /api/health
+ port: http
+ initialDelaySeconds: 60
+ periodSeconds: 30
resources:
requests:
- memory: "256Mi"
- cpu: "100m"
+ cpu: 200m
+ memory: 256Mi
limits:
- memory: "512Mi"
- cpu: "500m"
+ cpu: 500m
+ memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
- name: la-huasca-nextjs
- namespace: ${NAMESPACE}
+ name: la-huasca-ts
+ namespace: la-huasca-ts
spec:
- selector:
- app: la-huasca-nextjs
ports:
- port: 80
- targetPort: 3000
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app: la-huasca-ts
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
- name: la-huasca-nextjs
- namespace: ${NAMESPACE}
+ name: la-huasca-ts
+ namespace: la-huasca-ts
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
spec:
ingressClassName: traefik
+ tls:
+ - hosts:
+ - la-huasca.ajavasoft.com
+ secretName: la-huasca-ts-tls
rules:
- - host: lahuasca.com
+ - host: la-huasca.ajavasoft.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
- name: la-huasca-nextjs
- port:
- number: 80
- - host: www.lahuasca.com
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: la-huasca-nextjs
+ name: la-huasca-ts
port:
number: 80
diff --git a/k8s/deployment.json b/k8s/deployment.json
new file mode 100644
index 0000000..6aef13e
--- /dev/null
+++ b/k8s/deployment.json
@@ -0,0 +1,83 @@
+{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": {
+ "name": "la-huasca-ts",
+ "namespace": "la-huasca-ts"
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "matchLabels": {
+ "app": "la-huasca-ts"
+ }
+ },
+ "template": {
+ "metadata": {
+ "labels": {
+ "app": "la-huasca-ts"
+ }
+ },
+ "spec": {
+ "imagePullSecrets": [
+ {
+ "name": "la-huasca-registry"
+ }
+ ],
+ "containers": [
+ {
+ "name": "la-huasca-ts",
+ "image": "gitea.ajavasoft.com/muken/la-huasca-ts:latest",
+ "ports": [
+ {
+ "containerPort": 3000,
+ "name": "http"
+ }
+ ],
+ "env": [
+ {
+ "name": "NODE_ENV",
+ "value": "production"
+ },
+ {
+ "name": "DATABASE_URL",
+ "valueFrom": {
+ "secretKeyRef": {
+ "name": "calendar-db-secrets",
+ "key": "url"
+ }
+ }
+ }
+ ],
+ "readinessProbe": {
+ "httpGet": {
+ "path": "/api/health",
+ "port": "http"
+ },
+ "initialDelaySeconds": 20,
+ "periodSeconds": 15
+ },
+ "livenessProbe": {
+ "httpGet": {
+ "path": "/api/health",
+ "port": "http"
+ },
+ "initialDelaySeconds": 60,
+ "periodSeconds": 30
+ },
+ "resources": {
+ "requests": {
+ "cpu": "200m",
+ "memory": "256Mi"
+ },
+ "limits": {
+ "cpu": "500m",
+ "memory": "512Mi"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/k8s/ingress.json b/k8s/ingress.json
new file mode 100644
index 0000000..08851b5
--- /dev/null
+++ b/k8s/ingress.json
@@ -0,0 +1,43 @@
+{
+ "apiVersion": "networking.k8s.io/v1",
+ "kind": "Ingress",
+ "metadata": {
+ "name": "la-huasca-ts",
+ "namespace": "la-huasca-ts",
+ "annotations": {
+ "traefik.ingress.kubernetes.io/router.entrypoints": "web,websecure"
+ }
+ },
+ "spec": {
+ "ingressClassName": "traefik",
+ "tls": [
+ {
+ "hosts": [
+ "lahuasca.com"
+ ],
+ "secretName": "la-huasca-ts-tls"
+ }
+ ],
+ "rules": [
+ {
+ "host": "lahuasca.com",
+ "http": {
+ "paths": [
+ {
+ "path": "/",
+ "pathType": "Prefix",
+ "backend": {
+ "service": {
+ "name": "la-huasca-ts",
+ "port": {
+ "number": 80
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/k8s/service.json b/k8s/service.json
new file mode 100644
index 0000000..e77e33b
--- /dev/null
+++ b/k8s/service.json
@@ -0,0 +1,21 @@
+{
+ "apiVersion": "v1",
+ "kind": "Service",
+ "metadata": {
+ "name": "la-huasca-ts",
+ "namespace": "la-huasca-ts"
+ },
+ "spec": {
+ "ports": [
+ {
+ "port": 80,
+ "targetPort": "http",
+ "protocol": "TCP",
+ "name": "http"
+ }
+ ],
+ "selector": {
+ "app": "la-huasca-ts"
+ }
+ }
+}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index a78444d..fc9bbb5 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -329,14 +329,20 @@ export default function AdminPage() {
const savePlace = async (e: React.FormEvent) => {
e.preventDefault();
- const payload = { ...placeForm, photos: placeForm.photos || [], featured_photo: placeForm.featured_photo || null, active: placeForm.active !== false, sort_order: placeForm.sort_order || 0 };
+ const payload = {
+ ...placeForm,
+ photos: Array.isArray(placeForm.photos) ? placeForm.photos : [],
+ featured_photo: placeForm.featured_photo || null,
+ active: placeForm.active === true,
+ sort_order: placeForm.sort_order || 0,
+ };
if (editingPlace) {
- await api(`/api/places/${editingPlace}`, 'PUT', payload);
- setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? { ...p, ...payload } as Place : p)));
+ const r = await api(`/api/places/${editingPlace}`, 'PUT', payload);
+ setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? ({ ...p, ...payload } as Place) : p)));
setEditingPlace(null);
} else {
- const json = await api('/api/places', 'POST', payload);
- setPlaces((prev) => [...prev, json.data]);
+ const r = await api('/api/places', 'POST', payload);
+ setPlaces((prev) => [...prev, r.data]);
}
setPlaceForm({ name: '', description: '', photos: [], featured_photo: null, distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
notify('Place saved.');
@@ -490,7 +496,7 @@ export default function AdminPage() {
{error &&
{error}
}
{message && {message}
}
-
+ <AccordionSection title="Brand Assets">
@@ -504,7 +510,7 @@ export default function AdminPage() {
-
+ <AccordionSection title="Footer / Contact">
setFooter({ ...footer, facebook_url: v })} />
setFooter({ ...footer, instagram_url: v })} />
@@ -518,7 +524,7 @@ export default function AdminPage() {
-
-
+ <AccordionSection title="Homepage Slideshow">
-
+ <AccordionSection title="Calendar Events">
-
+ <AccordionSection title="Social Events">
-
+ <AccordionSection title="Rooms">
-
+ <AccordionSection title="Menu">
-
+ <AccordionSection title="Landscape Places">
-
+ <AccordionSection title="Pending Reviews">
{reviews.length === 0 ? No pending reviews.
: (
{reviews.map((r) => (
@@ -807,101 +813,65 @@ export default function AdminPage() {
))}
-
- )}
-
+ function AccordionSection({ title, children, defaultOpen = false }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) {
+ const [open, setOpen] = useState(defaultOpen);
+ return (
+
+ setOpen((v) => !v)}>
+
{title}
+ {open ? 'Minimize' : 'Maximize'}
+
+ {open && {children}
}
+
+ );
+ }
-
-
- [u.username, u.role, u.first_name || '', u.last_name || '', u.comments_disabled ? 'comments disabled' : ''])} onDelete={(i) => deleteUser(users[i].id)} onEdit={(i) => { setEditingUser(users[i].id); setUserEditForm({ first_name: users[i].first_name || '', last_name: users[i].last_name || '', comments_disabled: users[i].comments_disabled }); }} />
- {editingUser !== null && (
-
-
Edit user {users.find((u) => u.id === editingUser)?.username}
-
setUserEditForm({ ...userEditForm, first_name: v })} />
- setUserEditForm({ ...userEditForm, last_name: v })} />
-
-
-
- setEditingUser(null)}>Cancel
-
-
- )}
-
-
- );
-}
+ function Button({ children, type = 'button', disabled, onClick }: { children: React.ReactNode; type?: 'button' | 'submit'; disabled?: boolean; onClick?: () => void }) {
+ return (
+
+ );
+ }
-function Section({ title, children }: { title: string; children: React.ReactNode }) {
- return (
-
- );
-}
+ function SecondaryButton({ children, onClick, type = 'button' }: { children: React.ReactNode; onClick?: () => void; type?: 'button' | 'submit' }) {
+ return (
+
+ );
+ }
-function Button({ children, type = 'button', disabled, onClick }: { children: React.ReactNode; type?: 'button' | 'submit'; disabled?: boolean; onClick?: () => void }) {
- return (
-
- );
-}
-
-function SecondaryButton({ children, onClick, type = 'button' }: { children: React.ReactNode; onClick?: () => void; type?: 'button' | 'submit' }) {
- return (
-
- );
-}
-
-function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
- return (
+ function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
+ return (