Skip to content

Commit 7615c0d

Browse files
committed
feat: add delete button
1 parent 9d0db97 commit 7615c0d

File tree

7 files changed

+230
-10
lines changed

7 files changed

+230
-10
lines changed

lark-ui/src/lib/Button.svelte

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
2-
let { label, disabled = false, icon = undefined, color = '#F24B4B', onclick = () => {}, type = 'button', variant = 'default' }: {
3-
label: string;
2+
let { label = '', disabled = false, icon = undefined, color = '#F24B4B', onclick = () => {}, type = 'button', variant = 'default' }: {
3+
label?: string;
44
disabled?: boolean;
55
onclick?: () => void;
66
icon?: string;
@@ -35,8 +35,10 @@
3535
</script>
3636

3737
<button class="pushable pushable-{variant}" disabled={disabled} onclick={onclick} style="--color: {color}" type={type}>
38-
<span class="front front-{variant}">
39-
<p>{label}</p>
38+
<span class="front front-{variant}" class:icon-only={!label && icon}>
39+
{#if label}
40+
<p>{label}</p>
41+
{/if}
4042
{#if icon}
4143
<img src="/icons/{icon}.svg" alt="icon" />
4244
{/if}
@@ -88,6 +90,15 @@
8890
transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
8991
}
9092
93+
.front.icon-only {
94+
padding: 0 16px;
95+
}
96+
97+
.front.icon-only img {
98+
width: 24px;
99+
height: 24px;
100+
}
101+
91102
.front-default {
92103
color: white;
93104

lark-ui/src/lib/auth.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ export async function getProject(id: string, fetchFn: FetchFunction = fetch) {
142142
}
143143
}
144144

145+
export async function deleteProject(projectId: string, fetchFn: FetchFunction = fetch) {
146+
const response = await fetchFn(`${apiUrl}/api/projects/auth/${projectId}`, {
147+
method: 'DELETE',
148+
headers: { 'Content-Type': 'application/json' },
149+
credentials: 'include'
150+
});
151+
152+
if (response.ok) {
153+
return { success: true };
154+
} else {
155+
const error = await response.json().catch(() => ({ message: 'Failed to delete project' }));
156+
return { success: false, error: error.message || 'Failed to delete project' };
157+
}
158+
}
159+
145160
export async function updateProject(projectId: string, project: Partial<Project>, fetchFn: FetchFunction = fetch) {
146161
const response = await fetchFn(`${apiUrl}/api/projects/auth/${projectId}`, {
147162
method: 'PUT',

lark-ui/src/lib/hackatime/HackatimeProjectModal.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@
148148
{/if}
149149
{/await}
150150
{/if}
151-
<!-- <button onclick={onClose} class="close">
151+
<button onclick={onClose} class="close">
152152
<img src="/icons/remove.svg" alt="close" />
153-
</button> -->
153+
</button>
154154
</div>
155155
</div>
156156
</div>
@@ -188,6 +188,8 @@
188188
flex-direction: column;
189189
align-items: center;
190190
justify-content: space-between;
191+
padding-top: 20px;
192+
padding-bottom: 20px;
191193
}
192194
193195
.modal-title {
@@ -211,9 +213,19 @@
211213
212214
.close {
213215
position: absolute;
214-
top: 10px;
215-
right: 10px;
216+
top: 20px;
217+
right: 20px;
216218
cursor: pointer;
219+
background: transparent;
220+
border: none;
221+
padding: 8px;
222+
z-index: 10;
223+
}
224+
225+
.close img {
226+
filter: brightness(0) invert(1);
227+
width: 24px;
228+
height: 24px;
217229
}
218230
219231
.modal-text.error {

lark-ui/src/routes/app/projects/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<div class="no-projects">
6363
<h2 class="font-bold">You have no projects yet...</h2>
6464
<h3 class=""> Create one and it will appear here!</h3>
65-
<button on:click={() => goto("/app/projects/create")} class="pushable-blue mt-[2vh]">
65+
<button onclick={() => goto("/app/projects/create")} class="pushable-blue mt-[2vh]">
6666
<span
6767
class="front-blue font-['Moga',_sans-serif] text-[#fee1c0] text-[4vh] py-[1vh] text-center text-nowrap tracking-[3.84px] whitespace-pre"
6868
>

lark-ui/src/routes/app/projects/[id]/+page.svelte

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import Button from '$lib/Button.svelte';
33
import { goto } from '$app/navigation';
44
import { projectPageState } from './state.svelte';
5+
import { deleteProject, getProjects } from '$lib/auth';
56
67
const project = projectPageState.project;
78
@@ -35,6 +36,35 @@
3536
function openHackatimeAccountModal() {
3637
projectPageState.openHackatimeAccountModal = true;
3738
}
39+
40+
let deletingProject = $state(false);
41+
let showDeleteModal = $state(false);
42+
43+
function openDeleteModal() {
44+
showDeleteModal = true;
45+
}
46+
47+
function closeDeleteModal() {
48+
showDeleteModal = false;
49+
}
50+
51+
async function handleDeleteProject() {
52+
if (!projectPageState.project) return;
53+
54+
deletingProject = true;
55+
try {
56+
const result = await deleteProject(projectPageState.project.projectId);
57+
if (result.success) {
58+
await goto('/app/projects');
59+
} else {
60+
alert(result.error || 'Failed to delete project');
61+
deletingProject = false;
62+
}
63+
} catch (err) {
64+
alert(err instanceof Error ? err.message : 'Failed to delete project');
65+
deletingProject = false;
66+
}
67+
}
3868
</script>
3969

4070
<div class="project-details">
@@ -141,6 +171,23 @@
141171
{/if}
142172
</div>
143173

174+
{#if showDeleteModal}
175+
<div class="modal-overlay" onclick={closeDeleteModal}>
176+
<div class="modal-box" onclick={(e) => e.stopPropagation()} role="dialog">
177+
<div class="modal-content">
178+
<h2 class="modal-title">Delete Project?</h2>
179+
<p class="modal-text">
180+
Are you sure you want to delete this project? This action cannot be undone.
181+
</p>
182+
<div class="multi-button">
183+
<Button label="Cancel" color="blue" onclick={closeDeleteModal} disabled={deletingProject}/>
184+
<Button label={deletingProject ? "Deleting..." : "Delete"} color="red" onclick={handleDeleteProject} disabled={deletingProject}/>
185+
</div>
186+
</div>
187+
</div>
188+
</div>
189+
{/if}
190+
144191
{#if projectPageState.user && projectPageState.user.hackatimeAccount}
145192
{#if projectPageState.project?.submissions && projectPageState.project.submissions.length > 0}
146193
{@const latestSubmission = projectPageState.project.submissions[projectPageState.project.submissions.length - 1]}
@@ -160,12 +207,20 @@
160207
<div class="submit-section">
161208
<Button label="EDIT" icon="edit" color="blue" onclick={() => goto(`/app/projects/${projectPageState.project?.projectId}/edit`)}/>
162209
<Button label="Submit" onclick={() => goto(`/app/projects/${projectPageState.project?.projectId}/submit`)}/>
210+
{#if projectPageState.project?.submissions && projectPageState.project.submissions.length === 0}
211+
<Button label="" icon="remove" color="red" onclick={openDeleteModal} disabled={deletingProject}/>
212+
{/if}
163213
</div>
164214
{:else}
165215
<div class="submit-section-inital">
166216
<Button label="LINK HACKATIME Project" icon="link" color="blue" onclick={openHackatimeProjectModal}/>
167217
<img alt="required!" src="/handdrawn_text/required.png" style="width: 140px;" />
168218
</div>
219+
{#if projectPageState.project?.submissions && projectPageState.project.submissions.length === 0}
220+
<div class="submit-section">
221+
<Button label="" icon="remove" color="red" onclick={openDeleteModal} disabled={deletingProject}/>
222+
</div>
223+
{/if}
169224
{/if}
170225
{:else}
171226
<div class="submit-section-inital">
@@ -552,4 +607,96 @@
552607
font-size: 16px;
553608
}
554609
}
610+
611+
.modal-overlay {
612+
position: fixed;
613+
inset: 0;
614+
background: rgba(0, 0, 0, 0.5);
615+
display: flex;
616+
align-items: center;
617+
justify-content: center;
618+
z-index: 1000;
619+
}
620+
621+
.modal-box {
622+
position: relative;
623+
background-image: url("/shapes/shape-bg-1.svg");
624+
background-size: 100% 100%;
625+
background-repeat: no-repeat;
626+
width: 795px;
627+
height: 490px;
628+
max-width: 90vw;
629+
padding: 30px 37px;
630+
}
631+
632+
.modal-content {
633+
position: relative;
634+
width: 100%;
635+
height: 100%;
636+
display: flex;
637+
flex-direction: column;
638+
align-items: center;
639+
justify-content: space-between;
640+
}
641+
642+
.modal-title {
643+
font-family: "Moga", sans-serif;
644+
font-size: 44px;
645+
color: #453b61;
646+
text-align: center;
647+
letter-spacing: -0.352px;
648+
line-height: 1.5;
649+
margin: 0;
650+
}
651+
652+
.modal-text {
653+
font-family: "PT Sans", sans-serif;
654+
font-size: 24px;
655+
color: #453b61;
656+
text-align: center;
657+
letter-spacing: -0.264px;
658+
line-height: 1.5;
659+
margin: 0;
660+
}
661+
662+
.multi-button {
663+
display: flex;
664+
gap: 32px;
665+
}
666+
667+
@media (max-width: 820px) {
668+
.modal-content {
669+
padding: 25px 30px;
670+
gap: 40px;
671+
}
672+
673+
.modal-title {
674+
font-size: 32px;
675+
}
676+
677+
.modal-text {
678+
font-size: 20px;
679+
}
680+
}
681+
682+
@media (max-width: 480px) {
683+
.modal-content {
684+
padding: 20px 25px;
685+
gap: 30px;
686+
}
687+
688+
.modal-title {
689+
font-size: 24px;
690+
}
691+
692+
.modal-text {
693+
font-size: 16px;
694+
}
695+
696+
.multi-button {
697+
flex-direction: column;
698+
gap: 16px;
699+
width: 100%;
700+
}
701+
}
555702
</style>

owl-api/src/projects/projects.controller.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Controller, Post, Get, Put, Body, Param, UseGuards, Req, ParseIntPipe, Query } from '@nestjs/common';
1+
import { Controller, Post, Get, Put, Delete, Body, Param, UseGuards, Req, ParseIntPipe, Query } from '@nestjs/common';
22
import { Request } from 'express';
33
import { ProjectsService } from './projects.service';
44
import { CreateProjectDto } from './dto/create-project.dto';
@@ -90,4 +90,12 @@ export class ProjectsAuthController {
9090
) {
9191
return this.projectsService.getHackatimeProjects(id, req.user.userId);
9292
}
93+
94+
@Delete(':id')
95+
async deleteProject(
96+
@Param('id', ParseIntPipe) id: number,
97+
@Req() req: Request,
98+
) {
99+
return this.projectsService.deleteProject(id, req.user.userId);
100+
}
93101
}

owl-api/src/projects/projects.service.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,4 +750,31 @@ export class ProjectsService {
750750

751751
return leaderboard;
752752
}
753+
754+
async deleteProject(projectId: number, userId: number) {
755+
const project = await this.prisma.project.findUnique({
756+
where: { projectId },
757+
include: {
758+
submissions: true,
759+
},
760+
});
761+
762+
if (!project) {
763+
throw new NotFoundException('Project not found');
764+
}
765+
766+
if (project.userId !== userId) {
767+
throw new ForbiddenException('Access denied');
768+
}
769+
770+
if (project.submissions.length > 0) {
771+
throw new ForbiddenException('Cannot delete project with submissions');
772+
}
773+
774+
await this.prisma.project.delete({
775+
where: { projectId },
776+
});
777+
778+
return { deleted: true, projectId };
779+
}
753780
}

0 commit comments

Comments
 (0)