Skip to content

Commit b4aaa97

Browse files
feat: add projects page
1 parent ec2ffe9 commit b4aaa97

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

components/ProjectsList.vue

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<template>
2+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
3+
<!-- Header with Search and Filters -->
4+
<div class="mb-8">
5+
<h1 class="text-3xl font-bold text-gray-900 mb-6">Projects</h1>
6+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
7+
<div class="relative">
8+
<input
9+
type="text"
10+
v-model="searchQuery"
11+
placeholder="Search projects..."
12+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
13+
/>
14+
</div>
15+
<div class="relative">
16+
<select
17+
v-model="selectedStage"
18+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
19+
>
20+
<option value="">All Stages</option>
21+
<option v-for="stage in stages" :key="stage" :value="stage">
22+
{{ stage }}
23+
</option>
24+
</select>
25+
</div>
26+
</div>
27+
</div>
28+
29+
<!-- Loading and Error States -->
30+
<div v-if="pending" class="text-center py-12">
31+
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-blue-600"></div>
32+
<p class="mt-2 text-gray-600">Loading projects...</p>
33+
</div>
34+
35+
<div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4">
36+
<p class="text-red-700">Error loading projects: {{ error.message }}</p>
37+
</div>
38+
39+
<!-- Projects List -->
40+
<div v-else class="space-y-6">
41+
<div
42+
v-for="project in filteredProjects"
43+
:key="project.id"
44+
class="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden"
45+
>
46+
<div class="p-6">
47+
<div class="flex items-start justify-between">
48+
<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>
63+
</div>
64+
<div class="flex space-x-3">
65+
<a
66+
v-if="project.developers_url"
67+
:href="project.developers_url"
68+
target="_blank"
69+
class="text-gray-600 hover:text-gray-900"
70+
title="View on GitHub"
71+
>
72+
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
73+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.73.083-.73 1.205.085 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.295 24 12c0-6.63-5.37-12-12-12"/>
74+
</svg>
75+
</a>
76+
<a
77+
v-if="project.chat_channel"
78+
:href="`https://codeforphilly.org/chat/${project.chat_channel}`"
79+
target="_blank"
80+
class="text-gray-600 hover:text-gray-900"
81+
title="Join Chat"
82+
>
83+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
85+
</svg>
86+
</a>
87+
</div>
88+
</div>
89+
90+
<p class="mt-4 text-gray-600 line-clamp-3">{{ project.readme }}</p>
91+
92+
<div class="mt-4 flex flex-wrap gap-2">
93+
<span
94+
v-for="tag in project.tags"
95+
:key="tag.id"
96+
class="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-blue-50 text-blue-700"
97+
>
98+
{{ tag.title }}
99+
</span>
100+
</div>
101+
</div>
102+
</div>
103+
</div>
104+
</div>
105+
</template>
106+
107+
<script setup>
108+
import { ref, computed } from '#imports'
109+
110+
const searchQuery = ref('')
111+
const selectedStage = ref('')
112+
113+
const stages = [
114+
'Hibernating',
115+
'Prototyping',
116+
'Commenting',
117+
'Testing',
118+
'Maintaining',
119+
'Drifting'
120+
]
121+
122+
const stageClasses = {
123+
'Hibernating': 'bg-gray-100 text-gray-800',
124+
'Prototyping': 'bg-blue-100 text-blue-800',
125+
'Commenting': 'bg-green-100 text-green-800',
126+
'Testing': 'bg-yellow-100 text-yellow-800',
127+
'Maintaining': 'bg-purple-100 text-purple-800',
128+
'Drifting': 'bg-red-100 text-red-800'
129+
}
130+
131+
const formatDate = (dateString) => {
132+
if (!dateString) return 'N/A'
133+
return new Date(dateString).toLocaleDateString('en-US', {
134+
year: 'numeric',
135+
month: 'short',
136+
day: 'numeric'
137+
})
138+
}
139+
140+
const { data: projects, pending, error } = await useLazyAsyncData('all-projects', async () => {
141+
const client = useSupabaseClient()
142+
const { data, error } = await client
143+
.from('projects')
144+
.select(`
145+
*,
146+
project_tags (
147+
tags (
148+
id,
149+
title,
150+
class
151+
)
152+
)
153+
`)
154+
.order('modified_at', { ascending: false })
155+
156+
if (error) {
157+
throw error
158+
}
159+
160+
return data?.map(project => ({
161+
...project,
162+
tags: project.project_tags
163+
?.map(pt => pt.tags)
164+
.filter(tag => tag.class === 'tech') || []
165+
}))
166+
})
167+
168+
const filteredProjects = computed(() => {
169+
if (!projects.value) return []
170+
171+
return projects.value.filter(project => {
172+
const matchesSearch = !searchQuery.value ||
173+
project.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
174+
project.readme?.toLowerCase().includes(searchQuery.value.toLowerCase())
175+
176+
const matchesStage = !selectedStage.value ||
177+
project.stage === selectedStage.value
178+
179+
return matchesSearch && matchesStage
180+
})
181+
})
182+
</script>
183+
184+
<style scoped>
185+
.line-clamp-3 {
186+
display: -webkit-box;
187+
-webkit-line-clamp: 3;
188+
-webkit-box-orient: vertical;
189+
overflow: hidden;
190+
}
191+
</style>

pages/projects.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<template>
2+
<div class="min-h-screen bg-gray-50">
3+
<ProjectsList />
4+
</div>
5+
</template>
6+
7+
<script setup lang="ts">
8+
// Page component for /projects route
9+
definePageMeta({
10+
layout: 'default',
11+
title: 'Projects - Code for Philly'
12+
})
13+
</script>

0 commit comments

Comments
 (0)