|
1 | 1 | import { useEffect, useState, useMemo, useRef } from "react"; |
2 | 2 | import { useNavigate } from "react-router-dom"; |
3 | 3 | import { useAuth } from "../context/AuthContext"; |
4 | | -import { getUserProjects, deleteProject, renameProject, duplicateProject } from "../services/projectService"; |
| 4 | +import { getUserProjects, deleteProject, renameProject, duplicateProject, toggleProjectFeatured } from "../services/projectService"; |
5 | 5 | import type { Project } from "../services/projectService"; |
6 | 6 |
|
7 | 7 | type SortOption = "date-desc" | "date-asc" | "name"; |
@@ -130,14 +130,20 @@ function ProjectMenu({ |
130 | 130 | onRename, |
131 | 131 | onDuplicate, |
132 | 132 | onDelete, |
133 | | - isDeleting |
| 133 | + onToggleFeatured, |
| 134 | + isDeleting, |
| 135 | + isFeatured, |
| 136 | + isFeaturingLoading |
134 | 137 | }: { |
135 | 138 | isOpen: boolean; |
136 | 139 | onClose: () => void; |
137 | 140 | onRename: () => void; |
138 | 141 | onDuplicate: () => void; |
139 | 142 | onDelete: () => void; |
| 143 | + onToggleFeatured: () => void; |
140 | 144 | isDeleting: boolean; |
| 145 | + isFeatured: boolean; |
| 146 | + isFeaturingLoading: boolean; |
141 | 147 | }) { |
142 | 148 | const menuRef = useRef<HTMLDivElement>(null); |
143 | 149 |
|
@@ -178,6 +184,26 @@ function ProjectMenu({ |
178 | 184 | </svg> |
179 | 185 | Duplicate |
180 | 186 | </button> |
| 187 | + <button |
| 188 | + onClick={(e) => { e.stopPropagation(); onToggleFeatured(); }} |
| 189 | + disabled={isFeaturingLoading} |
| 190 | + className={`w-full px-3 py-2 text-left text-sm transition-colors flex items-center gap-2 disabled:opacity-50 ${isFeatured |
| 191 | + ? "text-yellow-400 hover:bg-yellow-500/10 hover:text-yellow-300" |
| 192 | + : "text-slate-300 hover:bg-slate-700 hover:text-white" |
| 193 | + }`} |
| 194 | + > |
| 195 | + {isFeaturingLoading ? ( |
| 196 | + <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"> |
| 197 | + <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> |
| 198 | + <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> |
| 199 | + </svg> |
| 200 | + ) : ( |
| 201 | + <svg className="w-4 h-4" fill={isFeatured ? "currentColor" : "none"} viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
| 202 | + <path strokeLinecap="round" strokeLinejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /> |
| 203 | + </svg> |
| 204 | + )} |
| 205 | + {isFeatured ? "Remove from Profile" : "Feature on Profile"} |
| 206 | + </button> |
181 | 207 | <div className="border-t border-slate-700 my-1" /> |
182 | 208 | <button |
183 | 209 | onClick={(e) => { e.stopPropagation(); onDelete(); }} |
@@ -208,6 +234,7 @@ function Projects() { |
208 | 234 | const [error, setError] = useState<string | null>(null); |
209 | 235 | const [deletingId, setDeletingId] = useState<string | null>(null); |
210 | 236 | const [duplicatingId, setDuplicatingId] = useState<string | null>(null); |
| 237 | + const [featuringId, setFeaturingId] = useState<string | null>(null); |
211 | 238 |
|
212 | 239 | // Search and Sort State |
213 | 240 | const [searchQuery, setSearchQuery] = useState(""); |
@@ -346,6 +373,24 @@ function Projects() { |
346 | 373 | } |
347 | 374 | }; |
348 | 375 |
|
| 376 | + const handleToggleFeatured = async (project: Project) => { |
| 377 | + setFeaturingId(project.id); |
| 378 | + setOpenMenuId(null); |
| 379 | + try { |
| 380 | + await toggleProjectFeatured(project.id, !project.isFeatured); |
| 381 | + setProjects((prev) => |
| 382 | + prev.map((p) => |
| 383 | + p.id === project.id ? { ...p, isFeatured: !p.isFeatured } : p |
| 384 | + ) |
| 385 | + ); |
| 386 | + } catch (err) { |
| 387 | + console.error("Error toggling featured:", err); |
| 388 | + alert("Failed to update featured status."); |
| 389 | + } finally { |
| 390 | + setFeaturingId(null); |
| 391 | + } |
| 392 | + }; |
| 393 | + |
349 | 394 | const formatDate = (timestamp: number | { toDate: () => Date } | null) => { |
350 | 395 | if (!timestamp) return "Never"; |
351 | 396 |
|
@@ -543,7 +588,10 @@ function Projects() { |
543 | 588 | onRename={() => handleRenameClick(project)} |
544 | 589 | onDuplicate={() => handleDuplicate(project)} |
545 | 590 | onDelete={() => handleDelete(project.id)} |
| 591 | + onToggleFeatured={() => handleToggleFeatured(project)} |
546 | 592 | isDeleting={deletingId === project.id} |
| 593 | + isFeatured={project.isFeatured ?? false} |
| 594 | + isFeaturingLoading={featuringId === project.id} |
547 | 595 | /> |
548 | 596 | </div> |
549 | 597 | </div> |
|
0 commit comments