diff --git a/.woodpecker.yml b/.woodpecker.yml index 00a0c53..7cc505b 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,12 +1,34 @@ +variables: + - ®istry registry.threesix.ai + - &project newtest + steps: - docker: + build-backend: image: woodpeckerci/plugin-kaniko settings: - registry: registry.threesix.ai - repo: "newtest" + registry: *registry + repo: "*project/backend" tags: - latest - ${CI_COMMIT_SHA:0:8} + context: backend + dockerfile: backend/Dockerfile + cache: true + skip-tls-verify: true + when: + - event: push + branch: main + + build-frontend: + image: woodpeckerci/plugin-kaniko + settings: + registry: *registry + repo: "*project/frontend" + tags: + - latest + - ${CI_COMMIT_SHA:0:8} + context: frontend + dockerfile: frontend/Dockerfile cache: true skip-tls-verify: true when: @@ -16,7 +38,13 @@ steps: deploy: image: bitnami/kubectl:latest commands: - - kubectl set image deployment/newtest newtest=registry.threesix.ai/newtest:${CI_COMMIT_SHA:0:8} -n projects + - kubectl set image deployment/newtest-backend backend=registry.threesix.ai/newtest/backend:${CI_COMMIT_SHA:0:8} -n projects + - kubectl set image deployment/newtest-frontend frontend=registry.threesix.ai/newtest/frontend:${CI_COMMIT_SHA:0:8} -n projects + - kubectl rollout status deployment/newtest-backend -n projects --timeout=120s + - kubectl rollout status deployment/newtest-frontend -n projects --timeout=120s when: - event: push branch: main + depends_on: + - build-backend + - build-frontend diff --git a/Dockerfile b/Dockerfile index e7846dc..d4d8552 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,118 @@ -# Default Dockerfile - replace with your application -FROM nginx:alpine +# Multi-stage build for full-stack task manager +# This Dockerfile builds both backend and frontend -# Copy static files or your app -COPY . /usr/share/nginx/html/ +# ============== BACKEND BUILD ============== +FROM golang:1.22-alpine AS backend-builder -EXPOSE 80 +WORKDIR /app/backend -CMD ["nginx", "-g", "daemon off;"] +RUN apk add --no-cache git + +COPY backend/go.mod backend/go.sum* ./ +RUN go mod download + +COPY backend/ . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# ============== FRONTEND BUILD ============== +FROM node:20-alpine AS frontend-deps +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm ci || npm install + +FROM node:20-alpine AS frontend-builder +WORKDIR /app + +COPY --from=frontend-deps /app/node_modules ./node_modules +COPY frontend/ . + +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# ============== FINAL STAGE ============== +FROM alpine:3.19 + +WORKDIR /app + +# Install nginx, nodejs, and supervisor +RUN apk --no-cache add ca-certificates nginx nodejs npm supervisor + +# Copy backend binary +COPY --from=backend-builder /app/backend/main ./backend/main + +# Copy frontend +COPY --from=frontend-builder /app/.next/standalone ./frontend/ +COPY --from=frontend-builder /app/.next/static ./frontend/.next/static +COPY --from=frontend-builder /app/public ./frontend/public + +# Create nginx config for reverse proxy +RUN mkdir -p /etc/nginx/http.d +COPY <) { + return ( + + +
+ {children} +
+ + + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..229531f --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { AddTaskDialog } from "@/components/add-task-dialog"; +import { TaskDetailDialog } from "@/components/task-detail-dialog"; +import { useToast } from "@/components/ui/use-toast"; +import { Plus, CheckCircle2, Clock, AlertCircle, Trash2 } from "lucide-react"; + +interface Task { + id: number; + title: string; + description: string; + status: string; + priority: string; + createdAt: string; + updatedAt: string; +} + +const statusConfig: Record = { + pending: { label: "Pending", icon: , variant: "secondary" }, + in_progress: { label: "In Progress", icon: , variant: "default" }, + completed: { label: "Completed", icon: , variant: "outline" }, +}; + +const priorityColors: Record = { + low: "bg-green-500/20 text-green-400 border-green-500/30", + medium: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30", + high: "bg-red-500/20 text-red-400 border-red-500/30", +}; + +export default function Dashboard() { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); + const { toast } = useToast(); + + const fetchTasks = async () => { + try { + const res = await fetch("/api/tasks"); + const data = await res.json(); + if (data.success) { + setTasks(data.data || []); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to fetch tasks", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTasks(); + }, []); + + const handleAddTask = async (task: { title: string; description: string; priority: string }) => { + try { + const res = await fetch("/api/tasks", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(task), + }); + const data = await res.json(); + if (data.success) { + toast({ + title: "Success", + description: "Task created successfully", + }); + fetchTasks(); + setAddDialogOpen(false); + } else { + throw new Error(data.error); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to create task", + variant: "destructive", + }); + } + }; + + const handleDeleteTask = async (id: number, e: React.MouseEvent) => { + e.stopPropagation(); + try { + const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" }); + const data = await res.json(); + if (data.success) { + toast({ + title: "Success", + description: "Task deleted successfully", + }); + fetchTasks(); + } else { + throw new Error(data.error); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to delete task", + variant: "destructive", + }); + } + }; + + const stats = { + total: tasks.length, + pending: tasks.filter((t) => t.status === "pending").length, + inProgress: tasks.filter((t) => t.status === "in_progress").length, + completed: tasks.filter((t) => t.status === "completed").length, + }; + + return ( +
+
+
+

Task Manager

+

Manage and track your tasks efficiently

+
+ +
+ + {/* Stats */} +
+ + + Total Tasks + {stats.total} + + + + + Pending + {stats.pending} + + + + + In Progress + {stats.inProgress} + + + + + Completed + {stats.completed} + + +
+ + {/* Task List */} + + + All Tasks + Click on a task to view details + + + {loading ? ( +
Loading tasks...
+ ) : tasks.length === 0 ? ( +
+ No tasks yet. Create your first task to get started. +
+ ) : ( +
+ {tasks.map((task) => { + const status = statusConfig[task.status] || statusConfig.pending; + return ( +
setSelectedTask(task)} + className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors" + > +
+
+ {task.title} + + {task.description || "No description"} + +
+
+
+ + {task.priority} + + + {status.icon} + {status.label} + + +
+
+ ); + })} +
+ )} +
+
+ + + + !open && setSelectedTask(null)} + /> +
+ ); +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..fa674c9 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/frontend/components/add-task-dialog.tsx b/frontend/components/add-task-dialog.tsx new file mode 100644 index 0000000..0920e5d --- /dev/null +++ b/frontend/components/add-task-dialog.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface AddTaskDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (task: { title: string; description: string; priority: string }) => void; +} + +export function AddTaskDialog({ open, onOpenChange, onSubmit }: AddTaskDialogProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [priority, setPriority] = useState("medium"); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!title.trim()) return; + + setLoading(true); + await onSubmit({ title, description, priority }); + setLoading(false); + setTitle(""); + setDescription(""); + setPriority("medium"); + }; + + return ( + + + + Add New Task + + Create a new task to track your work. Click save when you're done. + + +
+
+
+ + setTitle(e.target.value)} + placeholder="Enter task title" + required + /> +
+
+ +