build: Build a full-stack task management application with the following str...
This commit is contained in:
parent
c7f7f656c2
commit
c800501726
@ -3,9 +3,9 @@ FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
# Copy go mod files and download dependencies
|
||||
COPY go.mod go.sum* ./
|
||||
RUN go mod download
|
||||
RUN go mod download || go mod tidy
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
@ -25,7 +25,7 @@ RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8081
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the binary
|
||||
CMD ["./main"]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
module github.com/jordan/fs3/backend
|
||||
module backend
|
||||
|
||||
go 1.22
|
||||
|
||||
|
||||
@ -17,8 +17,10 @@ type Task struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Completed bool `json:"completed"`
|
||||
Status string `json:"status"`
|
||||
Priority string `json:"priority"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type TaskStore struct {
|
||||
@ -53,16 +55,26 @@ func (s *TaskStore) Get(id int) (Task, bool) {
|
||||
return task, ok
|
||||
}
|
||||
|
||||
func (s *TaskStore) Create(title, description string) Task {
|
||||
func (s *TaskStore) Create(title, description, status, priority string) Task {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
if priority == "" {
|
||||
priority = "medium"
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
task := Task{
|
||||
ID: s.nextID,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Completed: false,
|
||||
CreatedAt: time.Now(),
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
s.tasks[s.nextID] = task
|
||||
s.nextID++
|
||||
@ -96,9 +108,10 @@ func main() {
|
||||
store := NewTaskStore()
|
||||
|
||||
// Add some sample tasks
|
||||
store.Create("Welcome to Task Manager", "This is your first task. Click on it to see details!")
|
||||
store.Create("Add a new task", "Use the Add Task button to create new tasks")
|
||||
store.Create("Stay organized", "Keep track of all your tasks in one place")
|
||||
store.Create("Set up development environment", "Install all necessary tools and dependencies for the project", "completed", "high")
|
||||
store.Create("Design database schema", "Create the initial database schema for the application", "in-progress", "high")
|
||||
store.Create("Write unit tests", "Add comprehensive unit tests for all core functionality", "pending", "medium")
|
||||
store.Create("Deploy to production", "Set up CI/CD pipeline and deploy to cloud", "pending", "low")
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
@ -135,6 +148,8 @@ func main() {
|
||||
var input struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Priority string `json:"priority"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
@ -147,7 +162,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
task := store.Create(input.Title, input.Description)
|
||||
task := store.Create(input.Title, input.Description, input.Status, input.Priority)
|
||||
jsonResponse(w, http.StatusCreated, APIResponse{Success: true, Data: task})
|
||||
})
|
||||
|
||||
@ -190,7 +205,7 @@ func main() {
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: "OK"})
|
||||
})
|
||||
|
||||
port := ":8081"
|
||||
port := ":8080"
|
||||
log.Printf("Backend server starting on %s", port)
|
||||
if err := http.ListenAndServe(port, r); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -6,7 +6,7 @@ services:
|
||||
ports:
|
||||
- "8080:3000"
|
||||
environment:
|
||||
- BACKEND_URL=http://backend:8081
|
||||
- BACKEND_URL=http://backend:8080
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
@ -16,5 +16,5 @@ services:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8081:8081"
|
||||
- "8081:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
@ -7,7 +7,7 @@ const nextConfig = {
|
||||
source: '/api/:path*',
|
||||
destination: process.env.BACKEND_URL
|
||||
? `${process.env.BACKEND_URL}/api/:path*`
|
||||
: 'http://backend:8081/api/:path*',
|
||||
: 'http://backend:8080/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.5"
|
||||
},
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Trash2, Eye, CheckCircle2, Circle } from "lucide-react";
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
Eye,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -21,8 +29,49 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getTasks, createTask, deleteTask, getTask, Task } from "@/lib/api";
|
||||
|
||||
function getStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||
case "in-progress":
|
||||
return <Clock className="h-5 w-5 text-blue-500" />;
|
||||
default:
|
||||
return <Circle className="h-5 w-5 text-muted-foreground" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <Badge variant="success">Completed</Badge>;
|
||||
case "in-progress":
|
||||
return <Badge variant="info">In Progress</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">Pending</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
function getPriorityBadge(priority: string) {
|
||||
switch (priority) {
|
||||
case "high":
|
||||
return <Badge variant="destructive">High</Badge>;
|
||||
case "medium":
|
||||
return <Badge variant="warning">Medium</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">Low</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -31,6 +80,8 @@ export default function Home() {
|
||||
// Add task form state
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [newDescription, setNewDescription] = useState("");
|
||||
const [newStatus, setNewStatus] = useState("pending");
|
||||
const [newPriority, setNewPriority] = useState("medium");
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
@ -65,10 +116,17 @@ export default function Home() {
|
||||
|
||||
try {
|
||||
setAdding(true);
|
||||
const task = await createTask(newTitle.trim(), newDescription.trim());
|
||||
const task = await createTask(
|
||||
newTitle.trim(),
|
||||
newDescription.trim(),
|
||||
newStatus,
|
||||
newPriority
|
||||
);
|
||||
setTasks((prev) => [...prev, task]);
|
||||
setNewTitle("");
|
||||
setNewDescription("");
|
||||
setNewStatus("pending");
|
||||
setNewPriority("medium");
|
||||
setAddDialogOpen(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to add task");
|
||||
@ -159,6 +217,43 @@ export default function Home() {
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="status" className="text-sm font-medium">
|
||||
Status
|
||||
</label>
|
||||
<Select value={newStatus} onValueChange={setNewStatus}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="in-progress">
|
||||
In Progress
|
||||
</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="priority" className="text-sm font-medium">
|
||||
Priority
|
||||
</label>
|
||||
<Select
|
||||
value={newPriority}
|
||||
onValueChange={setNewPriority}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={adding || !newTitle.trim()}>
|
||||
@ -173,9 +268,12 @@ export default function Home() {
|
||||
{/* Error Alert */}
|
||||
{error && (
|
||||
<div className="bg-destructive/15 border border-destructive text-destructive px-4 py-3 rounded-lg mb-6">
|
||||
{error}
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
<button
|
||||
className="float-right font-bold"
|
||||
className="float-right font-bold -mt-6"
|
||||
onClick={() => setError(null)}
|
||||
>
|
||||
×
|
||||
@ -216,11 +314,7 @@ export default function Home() {
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{task.completed ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-5 w-5 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
{getStatusIcon(task.status)}
|
||||
<CardTitle className="text-lg leading-tight">
|
||||
{task.title}
|
||||
</CardTitle>
|
||||
@ -232,9 +326,10 @@ export default function Home() {
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(task.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
{getStatusBadge(task.status)}
|
||||
{getPriorityBadge(task.priority)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -254,6 +349,9 @@ export default function Home() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{new Date(task.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
@ -273,11 +371,7 @@ export default function Home() {
|
||||
<>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
{viewTask.completed ? (
|
||||
<CheckCircle2 className="h-6 w-6 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-6 w-6 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
{getStatusIcon(viewTask.status)}
|
||||
<DialogTitle className="text-xl">
|
||||
{viewTask.title}
|
||||
</DialogTitle>
|
||||
@ -292,25 +386,33 @@ export default function Home() {
|
||||
{viewTask.description || "No description provided"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div className="flex gap-4">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Status: </span>
|
||||
<span
|
||||
className={
|
||||
viewTask.completed
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"
|
||||
}
|
||||
>
|
||||
{viewTask.completed ? "Completed" : "Pending"}
|
||||
</span>
|
||||
<h4 className="text-sm font-medium text-muted-foreground mb-1">
|
||||
Status
|
||||
</h4>
|
||||
{getStatusBadge(viewTask.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground mb-1">
|
||||
Priority
|
||||
</h4>
|
||||
{getPriorityBadge(viewTask.priority)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created: </span>
|
||||
<span>
|
||||
{new Date(viewTask.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Updated: </span>
|
||||
<span>
|
||||
{new Date(viewTask.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="mt-6">
|
||||
|
||||
41
frontend/src/components/ui/badge.tsx
Normal file
41
frontend/src/components/ui/badge.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
success:
|
||||
"border-transparent bg-green-500/20 text-green-400 hover:bg-green-500/30",
|
||||
warning:
|
||||
"border-transparent bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30",
|
||||
info: "border-transparent bg-blue-500/20 text-blue-400 hover:bg-blue-500/30",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
160
frontend/src/components/ui/select.tsx
Normal file
160
frontend/src/components/ui/select.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
@ -2,8 +2,10 @@ export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
completed: boolean;
|
||||
status: "pending" | "in-progress" | "completed";
|
||||
priority: "low" | "medium" | "high";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface APIResponse<T> {
|
||||
@ -34,14 +36,16 @@ export async function getTask(id: number): Promise<Task> {
|
||||
|
||||
export async function createTask(
|
||||
title: string,
|
||||
description: string
|
||||
description: string,
|
||||
status: string = "pending",
|
||||
priority: string = "medium"
|
||||
): Promise<Task> {
|
||||
const res = await fetch(`${API_BASE}/tasks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ title, description }),
|
||||
body: JSON.stringify({ title, description, status, priority }),
|
||||
});
|
||||
const json: APIResponse<Task> = await res.json();
|
||||
if (!json.success) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user