repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Projectflow
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/projectflowgit clone gitlawb://did:key:z6Mkfh4Y.../projectflowb3cded1async from playground1d ago| #1 | import { useState } from "react"; |
| #2 | import { motion } from "framer-motion"; |
| #3 | import { |
| #4 | Plus, |
| #5 | FolderKanban, |
| #6 | MoreHorizontal, |
| #7 | Trash2, |
| #8 | Settings, |
| #9 | } from "lucide-react"; |
| #10 | import { useStore } from "../../application/stores"; |
| #11 | import { Modal, ProgressBar, EmptyState, Dropdown, DropdownItem } from "../ui"; |
| #12 | |
| #13 | export function ProjectsView() { |
| #14 | const { |
| #15 | projects, |
| #16 | issues, |
| #17 | setCurrentProject, |
| #18 | setActiveView, |
| #19 | createProject, |
| #20 | deleteProject, |
| #21 | } = useStore(); |
| #22 | |
| #23 | const [showCreate, setShowCreate] = useState(false); |
| #24 | const [newName, setNewName] = useState(""); |
| #25 | const [newDesc, setNewDesc] = useState(""); |
| #26 | const [newEmoji, setNewEmoji] = useState("📁"); |
| #27 | |
| #28 | const emojis = ["📁", "🌐", "📱", "⚙️", "🎨", "🚀", "💡", "🔧", "📊", "🎯"]; |
| #29 | |
| #30 | const handleCreate = () => { |
| #31 | if (!newName.trim()) return; |
| #32 | createProject({ |
| #33 | workspaceId: "w1", |
| #34 | name: newName, |
| #35 | description: newDesc, |
| #36 | emoji: newEmoji, |
| #37 | color: "#6366f1", |
| #38 | visibility: "public", |
| #39 | labels: [], |
| #40 | }); |
| #41 | setNewName(""); |
| #42 | setNewDesc(""); |
| #43 | setNewEmoji("📁"); |
| #44 | setShowCreate(false); |
| #45 | }; |
| #46 | |
| #47 | return ( |
| #48 | <div className="p-6 max-w-6xl mx-auto"> |
| #49 | <div className="page-header"> |
| #50 | <h1 className="page-title">Projects</h1> |
| #51 | <button onClick={() => setShowCreate(true)} className="btn-primary"> |
| #52 | <Plus size={16} /> |
| #53 | New Project |
| #54 | </button> |
| #55 | </div> |
| #56 | |
| #57 | {projects.length === 0 ? ( |
| #58 | <EmptyState |
| #59 | icon={<FolderKanban size={24} />} |
| #60 | title="No projects yet" |
| #61 | description="Create your first project to get started" |
| #62 | action={ |
| #63 | <button |
| #64 | onClick={() => setShowCreate(true)} |
| #65 | className="btn-primary" |
| #66 | > |
| #67 | <Plus size={16} /> |
| #68 | Create Project |
| #69 | </button> |
| #70 | } |
| #71 | /> |
| #72 | ) : ( |
| #73 | <motion.div |
| #74 | initial={{ opacity: 0 }} |
| #75 | animate={{ opacity: 1 }} |
| #76 | className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" |
| #77 | > |
| #78 | {projects.map((project, i) => { |
| #79 | const projectIssues = issues.filter( |
| #80 | (iss) => iss.projectId === project.id |
| #81 | ); |
| #82 | const completed = projectIssues.filter( |
| #83 | (iss) => iss.status === "done" |
| #84 | ).length; |
| #85 | const inProgress = projectIssues.filter( |
| #86 | (iss) => iss.status === "in-progress" |
| #87 | ).length; |
| #88 | |
| #89 | return ( |
| #90 | <motion.div |
| #91 | key={project.id} |
| #92 | initial={{ opacity: 0, y: 10 }} |
| #93 | animate={{ opacity: 1, y: 0 }} |
| #94 | transition={{ delay: i * 0.05 }} |
| #95 | className="card hover:border-border-strong transition-colors group cursor-pointer" |
| #96 | onClick={() => { |
| #97 | setCurrentProject(project.id); |
| #98 | setActiveView("board"); |
| #99 | }} |
| #100 | > |
| #101 | <div className="flex items-start justify-between mb-3"> |
| #102 | <div className="flex items-center gap-3"> |
| #103 | <span className="text-3xl">{project.emoji}</span> |
| #104 | <div> |
| #105 | <h3 className="text-sm font-semibold text-zinc-200 group-hover:text-zinc-100"> |
| #106 | {project.name} |
| #107 | </h3> |
| #108 | <p className="text-2xs text-zinc-500 mt-0.5"> |
| #109 | {project.visibility} |
| #110 | </p> |
| #111 | </div> |
| #112 | </div> |
| #113 | <Dropdown |
| #114 | trigger={ |
| #115 | <button |
| #116 | onClick={(e) => e.stopPropagation()} |
| #117 | className="p-1 rounded text-zinc-500 hover:text-zinc-300 hover:bg-surface-3 opacity-0 group-hover:opacity-100 transition-all" |
| #118 | > |
| #119 | <MoreHorizontal size={14} /> |
| #120 | </button> |
| #121 | } |
| #122 | > |
| #123 | <DropdownItem |
| #124 | onClick={() => { |
| #125 | setCurrentProject(project.id); |
| #126 | setActiveView("settings"); |
| #127 | }} |
| #128 | > |
| #129 | <Settings size={14} /> |
| #130 | Settings |
| #131 | </DropdownItem> |
| #132 | <DropdownItem |
| #133 | danger |
| #134 | onClick={() => { |
| #135 | if ( |
| #136 | confirm( |
| #137 | "Delete this project? This cannot be undone." |
| #138 | ) |
| #139 | ) { |
| #140 | deleteProject(project.id); |
| #141 | } |
| #142 | }} |
| #143 | > |
| #144 | <Trash2 size={14} /> |
| #145 | Delete |
| #146 | </DropdownItem> |
| #147 | </Dropdown> |
| #148 | </div> |
| #149 | |
| #150 | <p className="text-sm text-zinc-500 mb-4 line-clamp-2"> |
| #151 | {project.description} |
| #152 | </p> |
| #153 | |
| #154 | <div className="space-y-3"> |
| #155 | <div className="flex items-center justify-between text-2xs"> |
| #156 | <span className="text-zinc-500">Progress</span> |
| #157 | <span className="text-zinc-400"> |
| #158 | {completed}/{projectIssues.length} |
| #159 | </span> |
| #160 | </div> |
| #161 | <ProgressBar |
| #162 | value={completed} |
| #163 | max={projectIssues.length} |
| #164 | /> |
| #165 | <div className="flex items-center gap-3 text-2xs text-zinc-500"> |
| #166 | <span>{inProgress} in progress</span> |
| #167 | <span>·</span> |
| #168 | <span> |
| #169 | {projectIssues.length - completed - inProgress} remaining |
| #170 | </span> |
| #171 | </div> |
| #172 | </div> |
| #173 | </motion.div> |
| #174 | ); |
| #175 | })} |
| #176 | </motion.div> |
| #177 | )} |
| #178 | |
| #179 | <Modal |
| #180 | open={showCreate} |
| #181 | onClose={() => setShowCreate(false)} |
| #182 | title="Create Project" |
| #183 | > |
| #184 | <div className="space-y-4"> |
| #185 | <div> |
| #186 | <label className="block text-2xs font-medium text-zinc-400 uppercase tracking-wider mb-1.5"> |
| #187 | Icon |
| #188 | </label> |
| #189 | <div className="flex gap-2 flex-wrap"> |
| #190 | {emojis.map((e) => ( |
| #191 | <button |
| #192 | key={e} |
| #193 | onClick={() => setNewEmoji(e)} |
| #194 | className={`w-10 h-10 rounded-lg flex items-center justify-center text-xl transition-colors ${ |
| #195 | newEmoji === e |
| #196 | ? "bg-accent/20 ring-2 ring-accent" |
| #197 | : "bg-surface-3 hover:bg-surface-4" |
| #198 | }`} |
| #199 | > |
| #200 | {e} |
| #201 | </button> |
| #202 | ))} |
| #203 | </div> |
| #204 | </div> |
| #205 | |
| #206 | <div> |
| #207 | <label className="block text-2xs font-medium text-zinc-400 uppercase tracking-wider mb-1.5"> |
| #208 | Name |
| #209 | </label> |
| #210 | <input |
| #211 | value={newName} |
| #212 | onChange={(e) => setNewName(e.target.value)} |
| #213 | placeholder="My Project" |
| #214 | className="input" |
| #215 | autoFocus |
| #216 | /> |
| #217 | </div> |
| #218 | |
| #219 | <div> |
| #220 | <label className="block text-2xs font-medium text-zinc-400 uppercase tracking-wider mb-1.5"> |
| #221 | Description |
| #222 | </label> |
| #223 | <textarea |
| #224 | value={newDesc} |
| #225 | onChange={(e) => setNewDesc(e.target.value)} |
| #226 | placeholder="What is this project about?" |
| #227 | className="textarea" |
| #228 | rows={3} |
| #229 | /> |
| #230 | </div> |
| #231 | |
| #232 | <div className="flex justify-end gap-2 pt-2"> |
| #233 | <button |
| #234 | onClick={() => setShowCreate(false)} |
| #235 | className="btn-secondary" |
| #236 | > |
| #237 | Cancel |
| #238 | </button> |
| #239 | <button |
| #240 | onClick={handleCreate} |
| #241 | disabled={!newName.trim()} |
| #242 | className="btn-primary" |
| #243 | > |
| #244 | Create Project |
| #245 | </button> |
| #246 | </div> |
| #247 | </div> |
| #248 | </Modal> |
| #249 | </div> |
| #250 | ); |
| #251 | } |
| #252 |