Solo-Leveling-Self-Improvem.../app/quests/page.tsx

661 lines
23 KiB
TypeScript

"use client";
import type React from "react";
import Link from "next/link";
import {
ChevronLeft,
Search,
Trash2,
MoreVertical,
Edit,
ArrowUpDown,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Progress } from "@/components/ui/progress";
import { useUser } from "@/context/user-context";
import { useState, useEffect } from "react";
import { AddQuestForm } from "@/components/add-quest-form";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Quest } from "@/interface/quest.interface";
export default function QuestsPage() {
const {
userStats,
completeQuest,
updateQuestProgress,
deleteQuest,
updateQuest,
} = useUser();
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<"latest" | "priority">("latest");
// Load sorting preference from localStorage on component mount
useEffect(() => {
const savedSortBy = localStorage.getItem("questsSortBy");
if (
savedSortBy &&
(savedSortBy === "latest" || savedSortBy === "priority")
) {
setSortBy(savedSortBy);
}
}, []);
// Save sorting preference to localStorage whenever it changes
const handleSortChange = (value: "latest" | "priority") => {
setSortBy(value);
localStorage.setItem("questsSortBy", value);
};
// Sort function
const sortQuests = (quests: Quest[], isCompleted = false) => {
return quests.sort((a, b) => {
if (sortBy === "priority") {
// Sort by priority: High > Medium > Low (descending)
const priorityOrder = { High: 3, Medium: 2, Low: 1 };
const priorityA = priorityOrder[a.priority] || 0;
const priorityB = priorityOrder[b.priority] || 0;
// If priorities are the same, sort by creation time (newest first)
if (priorityA === priorityB) {
const timeA = a.createdAt || 0;
const timeB = b.createdAt || 0;
return timeB - timeA;
}
return priorityB - priorityA;
} else {
// Sort by time (newest first)
if (isCompleted) {
// For completed quests, sort by completion time
const timeA = a.completedAt || 0;
const timeB = b.completedAt || 0;
return timeB - timeA;
} else {
// For active quests, sort by creation time
const timeA = a.createdAt || 0;
const timeB = b.createdAt || 0;
return timeB - timeA;
}
}
});
};
// Filter and sort quests based on search term and status
const activeQuests = sortQuests(
userStats.quests.filter(
(quest) =>
!quest.completed &&
(quest.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
quest.description.toLowerCase().includes(searchTerm.toLowerCase()))
),
false
);
const completedQuests = sortQuests(
userStats.quests.filter(
(quest) =>
quest.completed &&
(quest.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
quest.description.toLowerCase().includes(searchTerm.toLowerCase()))
),
true
);
const handleDeleteQuest = (questId: string) => {
deleteQuest(questId);
};
return (
<div className="min-h-screen bg-[#0a0e14] text-[#e0f2ff] pb-16 md:pb-0">
<div className="container mx-auto px-4 py-6">
{/* Header */}
<header className="flex items-center mb-8">
<Link href="/" className="mr-4">
<Button variant="ghost" size="icon" className="hover:bg-[#1e2a3a]">
<ChevronLeft className="h-5 w-5" />
<span className="sr-only">Back</span>
</Button>
</Link>
<h1 className="text-2xl font-bold tracking-tight text-[#4cc9ff]">
Quests
</h1>
</header>{" "}
{/* Search and Filter */}
<div className="mb-6 space-y-4">
<div className="relative">
<div className="absolute inset-0 border border-[#4cc9ff]/30 rounded-lg shadow-[0_0_15px_rgba(76,201,255,0.15)]"></div>
<div className="p-4 relative z-10">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#8bacc1]" />
<Input
placeholder="Search quests..."
className="pl-9 bg-[#0a0e14] border-[#1e2a3a] focus-visible:ring-[#4cc9ff]"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
{/* Sorting Controls */}
<div className="relative">
<div className="absolute inset-0 border border-[#4cc9ff]/30 rounded-lg shadow-[0_0_15px_rgba(76,201,255,0.15)]"></div>
<div className="p-4 relative z-10">
<div className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4 text-[#8bacc1]" />
<span className="text-sm text-[#8bacc1]">Sort by:</span>{" "}
<Select value={sortBy} onValueChange={handleSortChange}>
<SelectTrigger className="w-[180px] bg-[#0a0e14] border-[#1e2a3a] focus-visible:ring-[#4cc9ff]">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#0a0e14] border-[#1e2a3a] text-[#e0f2ff]">
<SelectItem value="latest">Latest Created</SelectItem>
<SelectItem value="priority">
Priority (High to Low)
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
{/* Quests Tabs */}
<Tabs defaultValue="active">
<TabsList className="grid w-full grid-cols-2 bg-[#1e2a3a] border border-[#1e2a3a]">
<TabsTrigger
value="active"
className="data-[state=active]:bg-[#0a0e14] data-[state=active]:border-t-2 data-[state=active]:border-[#4cc9ff]"
>
Active
</TabsTrigger>
<TabsTrigger
value="completed"
className="data-[state=active]:bg-[#0a0e14] data-[state=active]:border-t-2 data-[state=active]:border-[#4cc9ff]"
>
Completed
</TabsTrigger>
</TabsList>
{/* Active Quests */}
<TabsContent value="active" className="mt-6">
<div className="mb-6 flex justify-center">
<AddQuestForm />
</div>
<div className="grid grid-cols-1 gap-4">
{activeQuests.length > 0 ? (
activeQuests.map((quest) => (
<QuestCard
key={quest.id}
quest={quest}
onComplete={() => completeQuest(quest.id)}
onProgress={(progress) =>
updateQuestProgress(quest.id, progress)
}
onDelete={() => handleDeleteQuest(quest.id)}
/>
))
) : (
<div className="text-center py-8 text-[#8bacc1]">
{searchTerm
? "No active quests match your search."
: "No active quests available."}
</div>
)}
</div>
</TabsContent>
{/* Completed Quests */}
<TabsContent value="completed" className="mt-6">
<div className="grid grid-cols-1 gap-4">
{completedQuests.length > 0 ? (
completedQuests.map((quest) => (
<QuestCard
key={quest.id}
quest={quest}
onDelete={() => handleDeleteQuest(quest.id)}
/>
))
) : (
<div className="text-center py-8 text-[#8bacc1]">
{searchTerm
? "No completed quests match your search."
: "No completed quests yet."}
</div>
)}
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
}
function QuestCard({
quest,
onComplete,
onProgress,
onDelete,
}: {
quest: Quest;
onComplete?: () => void;
onProgress?: (progress: number) => void;
onDelete?: () => void;
}) {
const { updateQuest } = useUser();
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editForm, setEditForm] = useState({
title: quest.title,
description: quest.description,
reward: quest.reward,
difficulty: quest.difficulty,
priority: quest.priority || "Medium",
expiry: quest.expiry,
expReward: quest.expReward,
statPointsReward: quest.statPointsReward,
});
// Format date from timestamp
const formatDate = (timestamp?: number) => {
if (!timestamp) return "";
const date = new Date(timestamp);
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "2-digit",
hour: "numeric",
minute: "2-digit",
});
};
const timeDisplay = quest.completed
? formatDate(quest.completedAt)
: formatDate(quest.createdAt);
const handleEditSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateQuest(quest.id, editForm);
setIsEditDialogOpen(false);
};
const difficultyColors = {
S: "bg-red-500",
A: "bg-orange-500",
B: "bg-yellow-500",
C: "bg-green-500",
D: "bg-blue-500",
E: "bg-purple-500",
};
const handleButtonClick = () => {
if (quest.completed) return;
if (quest.progress === 100 && onComplete) {
onComplete();
} else if (quest.progress < 100 && onProgress) {
// Increment progress by 25% each time
const newProgress = Math.min(100, quest.progress + 25);
onProgress(newProgress);
}
};
return (
<Card
className={`bg-[#0a0e14]/80 border-[#1e2a3a] relative ${
quest.completed ? "opacity-70" : ""
}`}
>
<div className="absolute inset-0 border border-[#4cc9ff]/10"></div>
<CardHeader className="pb-2 relative z-10">
{/* Action bar with difficulty, priority and menu - positioned above the title */}
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-2">
<div
className={`${
difficultyColors[quest.difficulty]
} w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold`}
>
{quest.difficulty}
</div>
{quest.priority && (
<div
className={`${
quest.priority === "High"
? "bg-red-500/20 text-red-400 border-red-500/30"
: quest.priority === "Medium"
? "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"
: "bg-green-500/20 text-green-400 border-green-500/30"
} px-2 py-0.5 rounded-full text-xs font-medium border`}
>
{quest.priority}
</div>
)}
</div>
{/* Dropdown menu for actions */}
{onDelete && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
<span className="sr-only">Actions</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-[#0a0e14] border-[#1e2a3a] text-[#e0f2ff]"
>
{quest.isCustom && !quest.completed && (
<DropdownMenuItem
className="focus:bg-[#1e2a3a]"
onSelect={(e) => {
e.preventDefault();
setIsEditDialogOpen(true);
}}
>
<Edit className="h-4 w-4 mr-2" />
Edit Quest
</DropdownMenuItem>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
className="text-red-400 focus:text-red-400 focus:bg-red-900/20"
onSelect={(e) => e.preventDefault()}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Quest
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent
className="bg-[#0a0e14] border-[#1e2a3a] text-[#e0f2ff] w-[90%] sm:max-w-md animate-solo-modal"
style={
{
"--solo-expand-duration": "0.5s",
"--solo-expand-easing": "cubic-bezier(0.16, 1, 0.3, 1)",
} as React.CSSProperties
}
>
<AlertDialogHeader>
<AlertDialogTitle className="text-[#4cc9ff]">
Delete Quest
</AlertDialogTitle>
<AlertDialogDescription className="text-[#8bacc1]">
Are you sure you want to delete this quest? This action
cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="bg-[#1e2a3a] text-[#e0f2ff] hover:bg-[#2a3a4a]">
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-900 text-[#e0f2ff] hover:bg-red-800"
onClick={onDelete}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuContent>
</DropdownMenu>
)}
{/* Edit Dialog */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent
className="bg-[#0a0e14] border-[#1e2a3a] text-[#e0f2ff] w-[90%] sm:max-w-md max-h-[90vh] overflow-y-auto animate-solo-modal"
style={
{
"--solo-expand-duration": "0.5s",
"--solo-expand-easing": "cubic-bezier(0.16, 1, 0.3, 1)",
} as React.CSSProperties
}
>
<DialogHeader>
<DialogTitle className="text-[#4cc9ff]">Edit Quest</DialogTitle>
</DialogHeader>
<form onSubmit={handleEditSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
value={editForm.title}
onChange={(e) =>
setEditForm({ ...editForm, title: e.target.value })
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={editForm.description}
onChange={(e) =>
setEditForm({ ...editForm, description: e.target.value })
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="reward">Reward</Label>
<Input
id="reward"
value={editForm.reward}
onChange={(e) =>
setEditForm({ ...editForm, reward: e.target.value })
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty">Difficulty</Label>
<Select
value={editForm.difficulty}
onValueChange={(value) =>
setEditForm({
...editForm,
difficulty: value as "S" | "A" | "B" | "C" | "D" | "E",
})
}
>
<SelectTrigger className="bg-[#0a0e14] border-[#1e2a3a]">
<SelectValue placeholder="Select difficulty" />
</SelectTrigger>
<SelectContent className="bg-[#0a0e14] border-[#1e2a3a]">
{["S", "A", "B", "C", "D", "E"].map((diff) => (
<SelectItem key={diff} value={diff}>
{diff}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority">Priority</Label>
<Select
value={editForm.priority}
onValueChange={(value) =>
setEditForm({
...editForm,
priority: value as "High" | "Medium" | "Low",
})
}
>
<SelectTrigger className="bg-[#0a0e14] border-[#1e2a3a]">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent className="bg-[#0a0e14] border-[#1e2a3a]">
<SelectItem value="High">High</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="Low">Low</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="expiry">Expiry</Label>
<Input
id="expiry"
value={editForm.expiry}
onChange={(e) =>
setEditForm({ ...editForm, expiry: e.target.value })
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="expReward">EXP Reward</Label>
<Input
id="expReward"
type="number"
value={editForm.expReward}
onChange={(e) =>
setEditForm({
...editForm,
expReward: parseInt(e.target.value),
})
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="statPointsReward">Stat Points</Label>
<Input
id="statPointsReward"
type="number"
value={editForm.statPointsReward}
onChange={(e) =>
setEditForm({
...editForm,
statPointsReward: parseInt(e.target.value),
})
}
className="bg-[#0a0e14] border-[#1e2a3a]"
/>
</div>
</div>
<DialogFooter>
<Button
type="submit"
className="bg-[#4cc9ff] text-[#0a0e14] hover:bg-[#4cc9ff]/90"
>
Save Changes
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Title and custom tag */}
<div className="flex items-center">
<CardTitle className="text-base">{quest.title}</CardTitle>
{quest.isCustom && (
<span className="ml-2 text-xs bg-[#1e2a3a] text-[#8bacc1] px-2 py-0.5 rounded-full">
Custom
</span>
)}
</div>
<CardDescription>{quest.description}</CardDescription>
</CardHeader>
<CardContent className="relative z-10">
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Progress</span>
<span>{quest.progress}%</span>
</div>
<Progress value={quest.progress} className="h-2 bg-[#1e2a3a]">
<div className="h-full bg-gradient-to-r from-[#4cc9ff] to-[#4cc9ff]/60 rounded-full" />
</Progress>
<div className="flex justify-between text-xs mt-2">
<div>
<span className="text-[#8bacc1]">Reward: </span>
<span className="text-[#4cc9ff]">{quest.reward}</span>
</div>
<div className="text-[#8bacc1]">{quest.expiry}</div>
</div>
<div className="flex justify-between text-xs">
<div>
<span className="text-[#8bacc1]">EXP: </span>
<span className="text-[#4cc9ff]">{quest.expReward}</span>
</div>
<div>
<span className="text-[#8bacc1]">Stat Points: </span>
<span className="text-[#4cc9ff]">{quest.statPointsReward}</span>
</div>
</div>
<div className="text-xs text-[#8bacc1]">
{quest.completed ? "Completed At: " : "Created At: "}
<span className="text-[#4cc9ff]">{timeDisplay}</span>
</div>
</div>
</CardContent>
<CardFooter className="relative z-10">
{!quest.completed && (
<Button
className="w-full bg-transparent border border-[#4cc9ff] hover:bg-[#4cc9ff]/10 text-[#4cc9ff]"
onClick={handleButtonClick}
>
{quest.progress === 0
? "Start Quest"
: quest.progress === 100
? "Claim Reward"
: "Update Progress"}
</Button>
)}
{quest.completed && (
<Button className="w-full bg-[#1e2a3a] hover:bg-[#2a3a4a]" disabled>
Completed
</Button>
)}
</CardFooter>
</Card>
);
}