Skip to content

Commit f7b5e90

Browse files
feat: scaffold project details page
1 parent 0730c3e commit f7b5e90

File tree

4 files changed

+317
-14
lines changed

4 files changed

+317
-14
lines changed

components/ProjectsList.vue

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,24 @@
4646
<div class="p-6">
4747
<div class="flex items-start justify-between">
4848
<div class="flex-1">
49-
<h2 class="text-xl font-semibold text-gray-900">{{ project.title }}</h2>
50-
<div class="mt-2 flex items-center space-x-2">
51-
<span
52-
:class="[
53-
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
54-
stageClasses[project.stage] || 'bg-gray-100 text-gray-800'
55-
]"
56-
>
57-
{{ project.stage }}
58-
</span>
59-
<span class="text-gray-500 text-sm">
60-
Last updated: {{ formatDate(project.modified_at) }}
61-
</span>
62-
</div>
49+
<NuxtLink :to="`/projects/${project.handle}`" class="group block">
50+
<h2 class="text-xl font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
51+
{{ project.title }}
52+
</h2>
53+
<div class="mt-2 flex items-center space-x-2">
54+
<span
55+
:class="[
56+
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
57+
stageClasses[project.stage] || 'bg-gray-100 text-gray-800'
58+
]"
59+
>
60+
{{ project.stage }}
61+
</span>
62+
<span class="text-gray-500 text-sm">
63+
Last updated: {{ formatDate(project.modified_at) }}
64+
</span>
65+
</div>
66+
</NuxtLink>
6367
</div>
6468
<div class="flex space-x-3">
6569
<a

package-lock.json

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
},
1212
"dependencies": {
1313
"@nuxtjs/supabase": "^1.5.0",
14+
"@types/dompurify": "^3.0.5",
15+
"@types/marked": "^5.0.2",
16+
"dompurify": "^3.2.4",
17+
"marked": "^15.0.7",
1418
"nuxt": "^3.16.0",
1519
"vue": "^3.5.13",
1620
"vue-router": "^4.5.0"

pages/projects/[handle].vue

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<template>
2+
<div v-if="project" class="container py-5">
3+
<div class="d-flex justify-content-between align-items-center mb-5">
4+
<h1 class="mb-0 fw-bold">{{ project.title }}</h1>
5+
<button class="btn btn-info px-4">
6+
<i class="bi bi-pencil-square me-2"></i>
7+
Edit Project
8+
</button>
9+
</div>
10+
11+
<div class="row g-4">
12+
<!-- Main content -->
13+
<div class="col-md-8">
14+
<section class="mb-5">
15+
<h2 class="h4 mb-3">Stage</h2>
16+
<div class="bg-primary text-white p-3 rounded-3 d-inline-block">
17+
<i class="bi bi-flag-fill me-2"></i>
18+
{{ project.stage }}
19+
</div>
20+
</section>
21+
22+
<section class="mb-5">
23+
<h2 class="h4 mb-3">README</h2>
24+
<div class="card border-0 shadow-sm">
25+
<div class="card-body p-4">
26+
<h3 class="h5 mb-4">Overview</h3>
27+
<div v-if="project.readme" class="readme-content" v-html="renderedReadme"></div>
28+
<div v-else class="text-muted fst-italic">No README content available.</div>
29+
</div>
30+
</div>
31+
</section>
32+
33+
<section v-if="project.tags?.length" class="mb-5">
34+
<h2 class="h4 mb-3">Tags</h2>
35+
<div class="d-flex flex-wrap gap-2">
36+
<span v-for="tag in project.tags" :key="tag.id"
37+
class="badge rounded-pill"
38+
:class="{
39+
'bg-primary': tag.class === 'tech',
40+
'bg-success': tag.class === 'topic',
41+
'bg-info': tag.class === 'event'
42+
}">
43+
<i :class="{
44+
'bi-code-slash': tag.class === 'tech',
45+
'bi-bookmark-fill': tag.class === 'topic',
46+
'bi-calendar-event': tag.class === 'event'
47+
}" class="bi me-1"></i>
48+
{{ tag.title }}
49+
</span>
50+
</div>
51+
</section>
52+
</div>
53+
54+
<!-- Sidebar -->
55+
<div class="col-md-4">
56+
<div class="card border-0 shadow-sm mb-4">
57+
<div class="card-header bg-light border-0 py-3">
58+
<h2 class="h5 mb-0">Project Info</h2>
59+
</div>
60+
<div class="card-body p-4">
61+
<div v-if="project.users_url" class="mb-3">
62+
<a :href="project.users_url" class="btn btn-primary w-100 py-2" target="_blank">
63+
<i class="bi bi-people-fill me-2"></i>Users' Site
64+
</a>
65+
</div>
66+
67+
<div v-if="project.developers_url" class="mb-3">
68+
<a :href="project.developers_url" class="btn btn-success w-100 py-2" target="_blank">
69+
<i class="bi bi-code-square me-2"></i>Developers' Site
70+
</a>
71+
</div>
72+
73+
<div v-if="project.chat_channel" class="mb-3">
74+
<a :href="'https://chat.codeforphilly.org/channel/' + project.chat_channel"
75+
class="btn btn-success w-100 py-2" target="_blank">
76+
<i class="bi bi-chat-dots-fill me-2"></i>Chat Channel
77+
<small class="d-block text-white-50 mt-1">#{{ project.chat_channel }}</small>
78+
</a>
79+
</div>
80+
</div>
81+
</div>
82+
83+
<div class="card border-0 shadow-sm">
84+
<div class="card-header bg-light border-0 py-3">
85+
<h2 class="h5 mb-0">Members</h2>
86+
</div>
87+
<div class="card-body p-4">
88+
<!-- TODO: Add members list once we have the data structure -->
89+
<button class="btn btn-success w-100 py-2">
90+
<i class="bi bi-plus-circle me-2"></i>Add
91+
</button>
92+
</div>
93+
</div>
94+
</div>
95+
</div>
96+
</div>
97+
<div v-else-if="error" class="container py-5">
98+
<div class="alert alert-danger shadow-sm">
99+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
100+
{{ error }}
101+
</div>
102+
</div>
103+
<div v-else class="container py-5">
104+
<div class="d-flex justify-content-center">
105+
<div class="spinner-border text-primary" role="status">
106+
<span class="visually-hidden">Loading...</span>
107+
</div>
108+
</div>
109+
</div>
110+
</template>
111+
112+
<script setup lang="ts">
113+
import { marked } from 'marked'
114+
import type { ProjectWithTags } from '~/types/supabase'
115+
116+
const route = useRoute()
117+
const handle = route.params.handle as string
118+
119+
// Get Supabase client
120+
const client = useSupabaseClient()
121+
122+
// Fetch project data with tags
123+
const { data: project, error } = await useLazyAsyncData<ProjectWithTags>(
124+
`project-${handle}`,
125+
async () => {
126+
const { data, error } = await client
127+
.from('projects')
128+
.select(`
129+
*,
130+
project_tags (
131+
tags (
132+
id,
133+
title,
134+
class
135+
)
136+
)
137+
`)
138+
.eq('handle', handle)
139+
.maybeSingle()
140+
141+
if (error) throw error
142+
if (!data) throw new Error('Project not found')
143+
144+
// Transform the nested tags data
145+
const tags = data.project_tags?.map(pt => pt.tags).filter(Boolean) || []
146+
const projectData = { ...data }
147+
delete projectData.project_tags
148+
return { ...projectData, tags }
149+
},
150+
{
151+
default: () => null
152+
}
153+
)
154+
155+
// Compute rendered README HTML
156+
const renderedReadme = computed(() => {
157+
if (!project.value?.readme) return ''
158+
return marked(project.value.readme)
159+
})
160+
</script>
161+
162+
<style scoped>
163+
.btn i {
164+
font-size: 1.1em;
165+
}
166+
167+
.badge {
168+
font-size: 0.9em;
169+
padding: 0.6em 1.2em;
170+
}
171+
172+
.readme-content {
173+
line-height: 1.6;
174+
}
175+
176+
.readme-content :deep(h1),
177+
.readme-content :deep(h2),
178+
.readme-content :deep(h3),
179+
.readme-content :deep(h4),
180+
.readme-content :deep(h5),
181+
.readme-content :deep(h6) {
182+
margin-top: 1.5em;
183+
margin-bottom: 0.75em;
184+
}
185+
186+
.readme-content :deep(p) {
187+
margin-bottom: 1em;
188+
}
189+
190+
.readme-content :deep(ul),
191+
.readme-content :deep(ol) {
192+
margin-bottom: 1em;
193+
padding-left: 2em;
194+
}
195+
196+
.readme-content :deep(li) {
197+
margin-bottom: 0.5em;
198+
}
199+
200+
.readme-content :deep(code) {
201+
background: #f8f9fa;
202+
padding: 0.2em 0.4em;
203+
border-radius: 0.25em;
204+
font-size: 0.9em;
205+
}
206+
207+
.readme-content :deep(pre) {
208+
background: #f8f9fa;
209+
padding: 1em;
210+
border-radius: 0.5em;
211+
margin-bottom: 1em;
212+
overflow-x: auto;
213+
}
214+
215+
.readme-content :deep(pre code) {
216+
background: none;
217+
padding: 0;
218+
}
219+
220+
.readme-content :deep(blockquote) {
221+
border-left: 4px solid #dee2e6;
222+
padding-left: 1em;
223+
margin-left: 0;
224+
margin-bottom: 1em;
225+
color: #6c757d;
226+
}
227+
228+
.readme-content :deep(img) {
229+
max-width: 100%;
230+
height: auto;
231+
border-radius: 0.5em;
232+
}
233+
234+
.readme-content :deep(table) {
235+
width: 100%;
236+
margin-bottom: 1em;
237+
border-collapse: collapse;
238+
}
239+
240+
.readme-content :deep(th),
241+
.readme-content :deep(td) {
242+
padding: 0.75em;
243+
border: 1px solid #dee2e6;
244+
}
245+
246+
.readme-content :deep(th) {
247+
background: #f8f9fa;
248+
}
249+
</style>

0 commit comments

Comments
 (0)