Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function ProjectTags({project, className}: {project: ProjectSummary; className:
);
}

function MemberStack({
export function MemberStack({
members,
emptyLabel = 'up for grabs',
}: {
Expand Down
6 changes: 5 additions & 1 deletion src/app/queries/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function useSaveProject(projectId?: string, claim = false) {
onSuccess: ({project}) => {
cache.setQueryData(['project', project.id], {project});
void cache.invalidateQueries({queryKey: ['projects', project.yearId]});
void cache.invalidateQueries({queryKey: ['year', project.yearId]});
void cache.invalidateQueries({queryKey: ['years']});
},
});
Expand All @@ -128,7 +129,10 @@ export function useDeleteProject() {
return useMutation({
mutationFn: (projectId: string) =>
apiRequest<void>(`/projects/${encodeURIComponent(projectId)}`, {method: 'DELETE'}),
onSuccess: () => void cache.invalidateQueries({queryKey: ['projects']}),
onSuccess: () => {
void cache.invalidateQueries({queryKey: ['projects']});
void cache.invalidateQueries({queryKey: ['year']});
},
});
}

Expand Down
30 changes: 29 additions & 1 deletion src/app/routes/ProjectsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {useEffect, useRef, useState} from 'react';
import {Link, useParams, useSearchParams} from 'wouter';

import type {BallotStatusResponse} from '../../shared/administration';
import type {ProjectSummary} from '../../shared/projects';
import {getAwardCategoryDescription} from '../awardCategories';
import {GroupManager} from '../components/GroupManager';
import {ProjectCard} from '../components/ProjectCard';
import {MemberStack, ProjectCard} from '../components/ProjectCard';
import {PageState, QueryState} from '../components/AppLayout';
import {useBallotStatus} from '../queries/administration';
import {useProjects, useYear} from '../queries/projects';
Expand Down Expand Up @@ -130,6 +131,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
? 'browse the finished projects, teams, and award winners.'
: 'see what everyone is building, join a team, or share an idea.'}
</p>
<MyProjectsStrip projects={year.data.myProjects} />
</div>
<div className="heroActions">
{(isAdmin || year.data.year.submissionsClosed) && (
Expand Down Expand Up @@ -348,6 +350,32 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
);
}

function MyProjectsStrip({projects}: {projects: ProjectSummary[]}) {
if (!projects.length) return null;
return (
<section className="myProjects" aria-label="your projects">
<p className="kicker">yours</p>
<div className="myProjectsTiles">
{projects.map((project) => (
<Link
className={`myProjectTile myProjectTile--${project.kind}`}
href={`/years/${project.yearId}/projects/${project.id}`}
key={project.id}
>
<span className="tag tag--group">
{project.kind === 'idea'
? 'open idea'
: (project.group?.name ?? 'ungrouped')}
</span>
<strong>{project.name}</strong>
<MemberStack members={project.members} />
</Link>
))}
</div>
</section>
);
}

function selectedCategoriesByProject(ballot?: BallotStatusResponse) {
const result = new Map<string, string[]>();
if (!ballot) return result;
Expand Down
83 changes: 82 additions & 1 deletion src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ main {
letter-spacing: -0.055em;
}
.archiveHero p:last-child,
.projectsHero > div > p:last-child,
.projectsHero > div > p:last-of-type,
.editorPage > header > p:last-child,
.operationsHero > p {
max-width: 34rem;
Expand All @@ -301,6 +301,64 @@ main {
font-size: 1rem;
line-height: 1.75;
}
.myProjects {
margin-top: 1.5rem;
}
.myProjects .kicker {
margin-bottom: 0.75rem;
}
.myProjectsTiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
gap: 0.65rem;
}
.myProjectTile {
display: grid;
gap: 0.4rem;
min-width: 0;
padding: 0.85rem 0.95rem;
color: inherit;
text-decoration: none;
border: 1px solid var(--line);
border-radius: 0.65rem;
background: #fff;
transition:
transform 120ms ease,
box-shadow 120ms ease,
border-color 120ms ease;
}
.myProjectTile:hover {
color: inherit;
border-color: var(--blurple);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(78, 42, 154, 0.1);
}
.myProjectTile--idea {
background: #fcfaff;
}
.myProjectTile .tag {
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
}
.myProjectTile strong {
overflow: hidden;
font-size: 0.95rem;
font-weight: 600;
line-height: 1.25;
letter-spacing: -0.02em;
text-overflow: ellipsis;
white-space: nowrap;
}
.myProjectTile .memberStack,
.myProjectTile .openSeat {
margin-top: 0.15rem;
}
.myProjectTile .memberStack > span {
width: 1.65rem;
height: 1.65rem;
}

.currentYearHero {
animation: rise 0.35s ease-out both;
Expand Down Expand Up @@ -923,6 +981,7 @@ main {
}
.projectRow {
display: grid;
position: relative;
grid-template-columns: minmax(12rem, 1fr) auto auto;
gap: 1rem;
align-items: center;
Expand All @@ -931,6 +990,14 @@ main {
border-bottom: 1px solid var(--line);
animation: rise 0.3s ease-out both;
}
.projectRow:has(h2 a) {
cursor: pointer;
}
.projectRow h2 a::after {
position: absolute;
inset: 0;
content: '';
}
.projectRow h2 {
min-width: 0;
margin: 0;
Expand Down Expand Up @@ -995,6 +1062,7 @@ main {
}
.projectCard {
display: flex;
position: relative;
min-width: 0;
min-height: 18rem;
padding: 1.25rem;
Expand All @@ -1004,6 +1072,7 @@ main {
background: #fff;
box-shadow: 0 1px 0 rgba(29, 17, 39, 0.03);
animation: rise 0.3s ease-out both;
cursor: pointer;
transition:
box-shadow 120ms ease,
transform 120ms ease;
Expand Down Expand Up @@ -1052,6 +1121,15 @@ main {
.projectCard h2 a {
text-decoration: none;
}
.projectCard h2 a::after {
position: absolute;
inset: 0;
content: '';
}
.projectCard > .markdown a {
position: relative;
z-index: 1;
}
.projectCard > .markdown {
display: -webkit-box;
overflow: hidden;
Expand Down Expand Up @@ -3420,6 +3498,9 @@ kbd {
align-items: stretch;
flex-direction: column;
}
.myProjectsTiles {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ballotOverview {
grid-template-columns: 1fr;
gap: 1.75rem;
Expand Down
1 change: 1 addition & 0 deletions src/shared/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface YearResponse {
year: YearSummary;
groups: GroupSummary[];
awards: AwardSummary[];
myProjects: ProjectSummary[];
}

export interface ProjectsResponse {
Expand Down
20 changes: 20 additions & 0 deletions src/worker/repositories/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ export async function listProjectOptions(db: D1Database, yearId: string) {
};
}

export async function listMyProjects(db: D1Database, yearId: string, userId: string) {
const {results} = await db
.prepare(
`${projectSelect()} WHERE p.year_id = ? AND p.status = 'active'
AND (p.creator_id = ? OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = ?
))
ORDER BY CASE p.kind WHEN 'project' THEN 0 ELSE 1 END,
p.name COLLATE NOCASE, p.id`,
)
.bind(yearId, userId, userId)
.all<ProjectRow>();
const members = await membersByProjectIds(
db,
results.map(({id}) => id),
);
return results.map((row) => mapProject(row, members.get(row.id) ?? []));
}

export async function listProjects(
db: D1Database,
options: {
Expand Down
5 changes: 4 additions & 1 deletion src/worker/routes/years.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {createGroup} from '../repositories/groups';
import {
getYear,
listGroups,
listMyProjects,
listProjectOptions,
listYears,
} from '../repositories/projects';
Expand All @@ -23,9 +24,10 @@ yearsRoutes.get('/', async (c) => {
yearsRoutes.get('/:yearId', async (c) => {
try {
const yearId = c.req.param('yearId');
const [year, groups, awardResult] = await Promise.all([
const [year, groups, myProjects, awardResult] = await Promise.all([
getYear(c.env.DB, yearId),
listGroups(c.env.DB, yearId),
listMyProjects(c.env.DB, yearId, c.get('user').id),
c.env.DB.prepare(
`SELECT a.id, a.year_id, a.project_id, p.name project_name,
a.category_id, category.name category_name, a.name
Expand All @@ -47,6 +49,7 @@ yearsRoutes.get('/:yearId', async (c) => {
const response: YearResponse = {
year,
groups,
myProjects,
awards: awardResult.results.map((award) => ({
id: award.id,
yearId: award.year_id,
Expand Down
Loading
Loading