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
3 changes: 3 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export default function Header() {
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link to="/dashboard">Dashboard</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings">Settings</Link>
</DropdownMenuItem>
Expand Down
21 changes: 21 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Route as UploadRouteImport } from './routes/upload'
import { Route as StarsRouteImport } from './routes/stars'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as SearchRouteImport } from './routes/search'
import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as AdminRouteImport } from './routes/admin'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SkillsIndexRouteImport } from './routes/skills/index'
Expand Down Expand Up @@ -41,6 +42,11 @@ const SearchRoute = SearchRouteImport.update({
path: '/search',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardRoute = DashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
const AdminRoute = AdminRouteImport.update({
id: '/admin',
path: '/admin',
Expand Down Expand Up @@ -80,6 +86,7 @@ const OwnerSlugRoute = OwnerSlugRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/dashboard': typeof DashboardRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
'/stars': typeof StarsRoute
Expand All @@ -93,6 +100,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/dashboard': typeof DashboardRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
'/stars': typeof StarsRoute
Expand All @@ -107,6 +115,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/dashboard': typeof DashboardRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
'/stars': typeof StarsRoute
Expand All @@ -122,6 +131,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/admin'
| '/dashboard'
| '/search'
| '/settings'
| '/stars'
Expand All @@ -135,6 +145,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/admin'
| '/dashboard'
| '/search'
| '/settings'
| '/stars'
Expand All @@ -148,6 +159,7 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/admin'
| '/dashboard'
| '/search'
| '/settings'
| '/stars'
Expand All @@ -162,6 +174,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRoute: typeof AdminRoute
DashboardRoute: typeof DashboardRoute
SearchRoute: typeof SearchRoute
SettingsRoute: typeof SettingsRoute
StarsRoute: typeof StarsRoute
Expand Down Expand Up @@ -203,6 +216,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SearchRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport
}
'/admin': {
id: '/admin'
path: '/admin'
Expand Down Expand Up @@ -258,6 +278,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRoute: AdminRoute,
DashboardRoute: DashboardRoute,
SearchRoute: SearchRoute,
SettingsRoute: SettingsRoute,
StarsRoute: StarsRoute,
Expand Down
120 changes: 120 additions & 0 deletions src/routes/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useQuery } from 'convex/react'
import { Package, Plus, Upload } from 'lucide-react'
import { api } from '../../convex/_generated/api'
import type { Doc } from '../../convex/_generated/dataModel'

export const Route = createFileRoute('/dashboard')({
component: Dashboard,
})

function Dashboard() {
const me = useQuery(api.users.me)
const mySkills = useQuery(
api.skills.list,
me?._id ? { ownerUserId: me._id, limit: 100 } : 'skip',
)

if (!me) {
return (
<main className="section">
<div className="card">Sign in to access your dashboard.</div>
</main>
)
}

const skills = mySkills ?? []
const ownerHandle = me.handle ?? null

return (
<main className="section">
<div className="dashboard-header">
<h1 className="section-title" style={{ margin: 0 }}>
My Skills
</h1>
<Link to="/upload" className="btn btn-primary">
<Plus className="h-4 w-4" aria-hidden="true" />
Upload New Skill
</Link>
</div>

{skills.length === 0 ? (
<div className="card dashboard-empty">
<Package className="dashboard-empty-icon" aria-hidden="true" />
<h2>No skills yet</h2>
<p>Upload your first skill to share it with the community.</p>
<Link to="/upload" className="btn btn-primary">
<Upload className="h-4 w-4" aria-hidden="true" />
Upload a Skill
</Link>
</div>
) : (
<div className="dashboard-grid">
{skills.map((skill) => (
<SkillCard key={skill._id} skill={skill} ownerHandle={ownerHandle} />
))}
</div>
)}
</main>
)
}

function SkillCard({
skill,
ownerHandle,
}: {
skill: Doc<'skills'>
ownerHandle: string | null
}) {
return (
<div className="dashboard-skill-card">
<div className="dashboard-skill-info">
{ownerHandle ? (
<Link
to="/$owner/$slug"
params={{ owner: ownerHandle, slug: skill.slug }}
className="dashboard-skill-name"
>
{skill.displayName}
</Link>
) : (
<Link to="/skills/$slug" params={{ slug: skill.slug }} className="dashboard-skill-name">
{skill.displayName}
</Link>
)}
<span className="dashboard-skill-slug">/{skill.slug}</span>
{skill.summary && (
<p className="dashboard-skill-description">{skill.summary}</p>
)}
<div className="dashboard-skill-stats">
<span>⤓ {skill.stats.downloads}</span>
<span>★ {skill.stats.stars}</span>
<span>{skill.stats.versions} v</span>
</div>
</div>
<div className="dashboard-skill-actions">
<Link
to="/upload"
search={{ updateSlug: skill.slug }}
className="btn btn-sm"
>
<Upload className="h-3 w-3" aria-hidden="true" />
New Version
</Link>
{ownerHandle ? (
<Link
to="/$owner/$slug"
params={{ owner: ownerHandle, slug: skill.slug }}
className="btn btn-ghost btn-sm"
>
View
</Link>
) : (
<Link to="/skills/$slug" params={{ slug: skill.slug }} className="btn btn-ghost btn-sm">
View
</Link>
)}
</div>
</div>
)
}
22 changes: 19 additions & 3 deletions src/routes/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useAction, useConvexAuth, useMutation } from 'convex/react'
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { useAction, useConvexAuth, useMutation, useQuery } from 'convex/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import semver from 'semver'
import { api } from '../../convex/_generated/api'
Expand All @@ -16,17 +16,25 @@ import {
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/

export const Route = createFileRoute('/upload')({
validateSearch: (search) => ({
updateSlug: typeof search.updateSlug === 'string' ? search.updateSlug : undefined,
}),
component: Upload,
})

export function Upload() {
const { isAuthenticated } = useConvexAuth()
const { updateSlug } = useSearch({ from: '/upload' })
const generateUploadUrl = useMutation(api.uploads.generateUploadUrl)
const publishVersion = useAction(api.skills.publishVersion)
const generateChangelogPreview = useAction(api.skills.generateChangelogPreview)
const existingSkill = useQuery(
api.skills.getBySlug,
updateSlug ? { slug: updateSlug } : 'skip',
)
const [hasAttempted, setHasAttempted] = useState(false)
const [files, setFiles] = useState<File[]>([])
const [slug, setSlug] = useState('')
const [slug, setSlug] = useState(updateSlug ?? '')
const [displayName, setDisplayName] = useState('')
const [version, setVersion] = useState('1.0.0')
const [tags, setTags] = useState('latest')
Expand Down Expand Up @@ -79,6 +87,14 @@ export function Upload() {
const trimmedName = displayName.trim()
const trimmedChangelog = changelog.trim()

useEffect(() => {
if (!existingSkill?.skill || !existingSkill?.latestVersion) return
setSlug(existingSkill.skill.slug)
setDisplayName(existingSkill.skill.displayName)
const nextVersion = semver.inc(existingSkill.latestVersion.version, 'patch')
if (nextVersion) setVersion(nextVersion)
}, [existingSkill])

useEffect(() => {
if (changelogTouchedRef.current) return
if (trimmedChangelog) return
Expand Down
Loading
Loading