Skip to content

Commit affbbe9

Browse files
committed
checkpoint
1 parent ea546e1 commit affbbe9

File tree

8 files changed

+222
-72
lines changed

8 files changed

+222
-72
lines changed

src/components/AuthenticatedUserMenu.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import { Link } from '@tanstack/react-router'
3-
import { ChevronDown, Settings, Lock, LogOut } from 'lucide-react'
3+
import { ChevronDown, Settings, Lock, LogOut, Sparkles } from 'lucide-react'
44
import { Avatar } from '~/components/Avatar'
55
import {
66
Dropdown,
@@ -52,6 +52,12 @@ export function AuthenticatedUserMenu({
5252
<span>Account</span>
5353
</Link>
5454
</DropdownItem>
55+
<DropdownItem asChild>
56+
<Link to="/showcase/mine" className="flex items-center gap-2">
57+
<Sparkles className="w-4 h-4" />
58+
<span>My Showcases</span>
59+
</Link>
60+
</DropdownItem>
5561
{canAdmin && (
5662
<DropdownItem asChild>
5763
<Link to="/admin" className="flex items-center gap-2">

src/components/MyShowcases.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import { getMyShowcasesQueryOptions } from '~/queries/showcases'
55
import { deleteShowcase } from '~/utils/showcase.functions'
66
import { libraries } from '~/libraries'
77
import { useToast } from './ToastProvider'
8-
import { Plus, Trash2, ExternalLink, Clock, Check, X } from 'lucide-react'
8+
import {
9+
Plus,
10+
Trash2,
11+
ExternalLink,
12+
Clock,
13+
Check,
14+
X,
15+
Pencil,
16+
} from 'lucide-react'
917
import type { Showcase } from '~/db/types'
10-
import { Button } from './Button'
18+
import { Button, buttonStyles } from './Button'
1119

1220
const libraryMap = new Map(libraries.map((lib) => [lib.id as string, lib]))
1321

@@ -174,6 +182,14 @@ export function MyShowcases() {
174182
<ExternalLink className="w-3 h-3" />
175183
Visit
176184
</Button>
185+
<Link
186+
to="/showcase/edit/$id"
187+
params={{ id: showcase.id }}
188+
className={buttonStyles}
189+
>
190+
<Pencil className="w-3 h-3" />
191+
Edit
192+
</Link>
177193
<Button onClick={() => handleDelete(showcase.id)}>
178194
<Trash2 className="w-3 h-3" />
179195
Delete

src/components/ShowcaseSubmitForm.tsx

Lines changed: 119 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as React from 'react'
22
import { useMutation } from '@tanstack/react-query'
33
import { useNavigate } from '@tanstack/react-router'
4-
import { submitShowcase } from '~/utils/showcase.functions'
4+
import { submitShowcase, updateShowcase } from '~/utils/showcase.functions'
55
import { libraries } from '~/libraries'
6-
import { SHOWCASE_USE_CASES, type ShowcaseUseCase } from '~/db/types'
6+
import {
7+
SHOWCASE_USE_CASES,
8+
type Showcase,
9+
type ShowcaseUseCase,
10+
} from '~/db/types'
711
import {
812
getAutoIncludedLibraries,
913
USE_CASE_LABELS,
@@ -19,52 +23,83 @@ const selectableLibraries = libraries.filter(
1923
lib.name && lib.id !== 'react-charts' && lib.id !== 'create-tsrouter-app',
2024
)
2125

22-
export function ShowcaseSubmitForm() {
26+
interface ShowcaseSubmitFormProps {
27+
showcase?: Showcase
28+
}
29+
30+
export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) {
2331
const navigate = useNavigate()
2432
const { notify } = useToast()
33+
const isEditMode = !!showcase
2534

26-
const [name, setName] = React.useState('')
27-
const [tagline, setTagline] = React.useState('')
28-
const [description, setDescription] = React.useState('')
29-
const [url, setUrl] = React.useState('')
30-
const [logoUrl, setLogoUrl] = React.useState<string | undefined>()
31-
const [screenshotUrl, setScreenshotUrl] = React.useState<string | undefined>()
32-
const [selectedLibraries, setSelectedLibraries] = React.useState<string[]>([])
35+
const [name, setName] = React.useState(showcase?.name ?? '')
36+
const [tagline, setTagline] = React.useState(showcase?.tagline ?? '')
37+
const [description, setDescription] = React.useState(
38+
showcase?.description ?? '',
39+
)
40+
const [url, setUrl] = React.useState(showcase?.url ?? '')
41+
const [logoUrl, setLogoUrl] = React.useState<string | undefined>(
42+
showcase?.logoUrl ?? undefined,
43+
)
44+
const [screenshotUrl, setScreenshotUrl] = React.useState<string | undefined>(
45+
showcase?.screenshotUrl ?? undefined,
46+
)
47+
const [selectedLibraries, setSelectedLibraries] = React.useState<string[]>(
48+
showcase?.libraries ?? [],
49+
)
3350
const [selectedUseCases, setSelectedUseCases] = React.useState<
3451
ShowcaseUseCase[]
35-
>([])
52+
>(showcase?.useCases ?? [])
3653

3754
// Get auto-included libraries based on selection
3855
const autoIncluded = React.useMemo(
3956
() => getAutoIncludedLibraries(selectedLibraries),
4057
[selectedLibraries],
4158
)
4259

43-
const submitMutation = useMutation({
60+
const onSuccess = () => {
61+
notify(
62+
<div>
63+
<div className="font-medium">
64+
{isEditMode ? 'Showcase updated!' : 'Showcase submitted!'}
65+
</div>
66+
<div className="text-gray-500 dark:text-gray-400 text-xs">
67+
{isEditMode
68+
? 'Your changes are pending review. Votes have been preserved.'
69+
: "Your project is pending review. We'll notify you when it's approved."}
70+
</div>
71+
</div>,
72+
)
73+
navigate({ to: '/showcase/mine' })
74+
}
75+
76+
const onError = (error: Error) => {
77+
notify(
78+
<div>
79+
<div className="font-medium">
80+
{isEditMode ? 'Update failed' : 'Submission failed'}
81+
</div>
82+
<div className="text-gray-500 dark:text-gray-400 text-xs">
83+
{error.message}
84+
</div>
85+
</div>,
86+
)
87+
}
88+
89+
const createMutation = useMutation({
4490
mutationFn: submitShowcase,
45-
onSuccess: () => {
46-
notify(
47-
<div>
48-
<div className="font-medium">Showcase submitted!</div>
49-
<div className="text-gray-500 dark:text-gray-400 text-xs">
50-
Your project is pending review. We'll notify you when it's approved.
51-
</div>
52-
</div>,
53-
)
54-
navigate({ to: '/showcase/mine' })
55-
},
56-
onError: (error: Error) => {
57-
notify(
58-
<div>
59-
<div className="font-medium">Submission failed</div>
60-
<div className="text-gray-500 dark:text-gray-400 text-xs">
61-
{error.message}
62-
</div>
63-
</div>,
64-
)
65-
},
91+
onSuccess,
92+
onError,
93+
})
94+
95+
const editMutation = useMutation({
96+
mutationFn: updateShowcase,
97+
onSuccess,
98+
onError,
6699
})
67100

101+
const isPending = createMutation.isPending || editMutation.isPending
102+
68103
const toggleLibrary = (libraryId: string) => {
69104
// Can't toggle auto-included libraries
70105
if (autoIncluded[libraryId]) return
@@ -105,29 +140,46 @@ export function ShowcaseSubmitForm() {
105140
return
106141
}
107142

108-
submitMutation.mutate({
109-
data: {
110-
name,
111-
tagline,
112-
description: description || undefined,
113-
url,
114-
logoUrl,
115-
screenshotUrl,
116-
libraries: selectedLibraries,
117-
useCases: selectedUseCases,
118-
},
119-
})
143+
// Warn user if editing an approved showcase
144+
if (isEditMode && showcase.status === 'approved') {
145+
const confirmed = confirm(
146+
'Saving changes will reset your showcase to pending review until re-approved. Your votes will be preserved. Continue?',
147+
)
148+
if (!confirmed) {
149+
return
150+
}
151+
}
152+
153+
const formData = {
154+
name,
155+
tagline,
156+
description: description || undefined,
157+
url,
158+
logoUrl,
159+
screenshotUrl,
160+
libraries: selectedLibraries,
161+
useCases: selectedUseCases,
162+
}
163+
164+
if (isEditMode) {
165+
editMutation.mutate({
166+
data: { ...formData, showcaseId: showcase.id },
167+
})
168+
} else {
169+
createMutation.mutate({ data: formData })
170+
}
120171
}
121172

122173
return (
123174
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
124175
<div className="max-w-2xl mx-auto px-4 py-12">
125176
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
126-
Submit Your Project
177+
{isEditMode ? 'Edit Your Project' : 'Submit Your Project'}
127178
</h1>
128179
<p className="mt-2 text-gray-600 dark:text-gray-400">
129-
Share what you've built with TanStack libraries. Your submission will
130-
be reviewed before appearing in the showcase.
180+
{isEditMode
181+
? 'Update your showcase submission. Changes will require re-approval but votes will be preserved.'
182+
: "Share what you've built with TanStack libraries. Your submission will be reviewed before appearing in the showcase."}
131183
</p>
132184

133185
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
@@ -306,17 +358,30 @@ export function ShowcaseSubmitForm() {
306358
</div>
307359

308360
{/* Submit Button */}
309-
<div className="pt-4">
361+
<div className="pt-4 flex gap-3">
362+
{isEditMode && (
363+
<Button
364+
type="button"
365+
onClick={() => navigate({ to: '/showcase/mine' })}
366+
className="flex-1 justify-center px-6 py-3 font-medium rounded-lg"
367+
>
368+
Cancel
369+
</Button>
370+
)}
310371
<Button
311372
type="submit"
312373
disabled={
313-
submitMutation.isPending ||
314-
selectedLibraries.length === 0 ||
315-
!screenshotUrl
374+
isPending || selectedLibraries.length === 0 || !screenshotUrl
316375
}
317-
className="w-full justify-center px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium rounded-lg border-none"
376+
className={`${isEditMode ? 'flex-1' : 'w-full'} justify-center px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium rounded-lg border-none`}
318377
>
319-
{submitMutation.isPending ? 'Submitting...' : 'Submit for Review'}
378+
{isPending
379+
? isEditMode
380+
? 'Saving...'
381+
: 'Submitting...'
382+
: isEditMode
383+
? 'Save Changes'
384+
: 'Submit for Review'}
320385
</Button>
321386
</div>
322387
</form>

src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { Route as LibrariesFeedIndexRouteImport } from './routes/_libraries/feed
6565
import { Route as LibrariesBlogIndexRouteImport } from './routes/_libraries/blog.index'
6666
import { Route as LibrariesAccountIndexRouteImport } from './routes/_libraries/account/index'
6767
import { Route as StatsNpmPackagesRouteImport } from './routes/stats/npm/$packages'
68+
import { Route as ShowcaseEditIdRouteImport } from './routes/showcase/edit.$id'
6869
import { Route as AuthProviderStartRouteImport } from './routes/auth/$provider/start'
6970
import { Route as ApiGithubWebhookRouteImport } from './routes/api/github/webhook'
7071
import { Route as ApiDiscordInteractionsRouteImport } from './routes/api/discord/interactions'
@@ -385,6 +386,11 @@ const StatsNpmPackagesRoute = StatsNpmPackagesRouteImport.update({
385386
path: '/stats/npm/$packages',
386387
getParentRoute: () => rootRouteImport,
387388
} as any)
389+
const ShowcaseEditIdRoute = ShowcaseEditIdRouteImport.update({
390+
id: '/showcase/edit/$id',
391+
path: '/showcase/edit/$id',
392+
getParentRoute: () => rootRouteImport,
393+
} as any)
388394
const AuthProviderStartRoute = AuthProviderStartRouteImport.update({
389395
id: '/auth/$provider/start',
390396
path: '/auth/$provider/start',
@@ -663,6 +669,7 @@ export interface FileRoutesByFullPath {
663669
'/api/discord/interactions': typeof ApiDiscordInteractionsRoute
664670
'/api/github/webhook': typeof ApiGithubWebhookRoute
665671
'/auth/$provider/start': typeof AuthProviderStartRoute
672+
'/showcase/edit/$id': typeof ShowcaseEditIdRoute
666673
'/stats/npm/$packages': typeof StatsNpmPackagesRoute
667674
'/account/': typeof LibrariesAccountIndexRoute
668675
'/blog/': typeof LibrariesBlogIndexRoute
@@ -754,6 +761,7 @@ export interface FileRoutesByTo {
754761
'/api/discord/interactions': typeof ApiDiscordInteractionsRoute
755762
'/api/github/webhook': typeof ApiGithubWebhookRoute
756763
'/auth/$provider/start': typeof AuthProviderStartRoute
764+
'/showcase/edit/$id': typeof ShowcaseEditIdRoute
757765
'/stats/npm/$packages': typeof StatsNpmPackagesRoute
758766
'/account': typeof LibrariesAccountIndexRoute
759767
'/blog': typeof LibrariesBlogIndexRoute
@@ -852,6 +860,7 @@ export interface FileRoutesById {
852860
'/api/discord/interactions': typeof ApiDiscordInteractionsRoute
853861
'/api/github/webhook': typeof ApiGithubWebhookRoute
854862
'/auth/$provider/start': typeof AuthProviderStartRoute
863+
'/showcase/edit/$id': typeof ShowcaseEditIdRoute
855864
'/stats/npm/$packages': typeof StatsNpmPackagesRoute
856865
'/_libraries/account/': typeof LibrariesAccountIndexRoute
857866
'/_libraries/blog/': typeof LibrariesBlogIndexRoute
@@ -950,6 +959,7 @@ export interface FileRouteTypes {
950959
| '/api/discord/interactions'
951960
| '/api/github/webhook'
952961
| '/auth/$provider/start'
962+
| '/showcase/edit/$id'
953963
| '/stats/npm/$packages'
954964
| '/account/'
955965
| '/blog/'
@@ -1041,6 +1051,7 @@ export interface FileRouteTypes {
10411051
| '/api/discord/interactions'
10421052
| '/api/github/webhook'
10431053
| '/auth/$provider/start'
1054+
| '/showcase/edit/$id'
10441055
| '/stats/npm/$packages'
10451056
| '/account'
10461057
| '/blog'
@@ -1138,6 +1149,7 @@ export interface FileRouteTypes {
11381149
| '/api/discord/interactions'
11391150
| '/api/github/webhook'
11401151
| '/auth/$provider/start'
1152+
| '/showcase/edit/$id'
11411153
| '/stats/npm/$packages'
11421154
| '/_libraries/account/'
11431155
| '/_libraries/blog/'
@@ -1198,6 +1210,7 @@ export interface RootRouteChildren {
11981210
ApiDiscordInteractionsRoute: typeof ApiDiscordInteractionsRoute
11991211
ApiGithubWebhookRoute: typeof ApiGithubWebhookRoute
12001212
AuthProviderStartRoute: typeof AuthProviderStartRoute
1213+
ShowcaseEditIdRoute: typeof ShowcaseEditIdRoute
12011214
StatsNpmPackagesRoute: typeof StatsNpmPackagesRoute
12021215
StatsNpmIndexRoute: typeof StatsNpmIndexRoute
12031216
ApiAuthCallbackProviderRoute: typeof ApiAuthCallbackProviderRoute
@@ -1597,6 +1610,13 @@ declare module '@tanstack/react-router' {
15971610
preLoaderRoute: typeof StatsNpmPackagesRouteImport
15981611
parentRoute: typeof rootRouteImport
15991612
}
1613+
'/showcase/edit/$id': {
1614+
id: '/showcase/edit/$id'
1615+
path: '/showcase/edit/$id'
1616+
fullPath: '/showcase/edit/$id'
1617+
preLoaderRoute: typeof ShowcaseEditIdRouteImport
1618+
parentRoute: typeof rootRouteImport
1619+
}
16001620
'/auth/$provider/start': {
16011621
id: '/auth/$provider/start'
16021622
path: '/auth/$provider/start'
@@ -2120,6 +2140,7 @@ const rootRouteChildren: RootRouteChildren = {
21202140
ApiDiscordInteractionsRoute: ApiDiscordInteractionsRoute,
21212141
ApiGithubWebhookRoute: ApiGithubWebhookRoute,
21222142
AuthProviderStartRoute: AuthProviderStartRoute,
2143+
ShowcaseEditIdRoute: ShowcaseEditIdRoute,
21232144
StatsNpmPackagesRoute: StatsNpmPackagesRoute,
21242145
StatsNpmIndexRoute: StatsNpmIndexRoute,
21252146
ApiAuthCallbackProviderRoute: ApiAuthCallbackProviderRoute,

0 commit comments

Comments
 (0)