feat: add shared @sexy.pivoine.art/types package and fix type safety across frontend/backend
- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.) - Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:* - Update backend Pothos objectRef types to use shared interfaces instead of inline types - Update frontend $lib/types.ts to re-export from shared package - Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[]) - Add artist_name to comment user select in backend resolver - Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,8 @@
|
||||
.filter((article) => {
|
||||
const matchesSearch =
|
||||
article.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
article.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
article.author.first_name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
article.excerpt?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
article.author?.first_name?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory = categoryFilter === "all" || article.category === categoryFilter;
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
@@ -189,12 +189,12 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
src={getAssetUrl(featuredArticle.author.avatar, "mini")}
|
||||
alt={featuredArticle.author.first_name}
|
||||
src={getAssetUrl(featuredArticle.author?.avatar, "mini")}
|
||||
alt={featuredArticle.author?.first_name}
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-medium">{featuredArticle.author.first_name}</p>
|
||||
<p class="font-medium">{featuredArticle.author?.first_name}</p>
|
||||
<div class="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span>{timeAgo.format(new Date(featuredArticle.publish_date))}</span>
|
||||
<span>•</span>
|
||||
@@ -273,7 +273,7 @@
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{#each article.tags.slice(0, 3) as tag (tag)}
|
||||
{#each (article.tags ?? []).slice(0, 3) as tag (tag)}
|
||||
<a
|
||||
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
||||
href="/tags/{tag}"
|
||||
@@ -287,12 +287,12 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src={getAssetUrl(article.author.avatar, "mini")}
|
||||
alt={article.author.first_name}
|
||||
src={getAssetUrl(article.author?.avatar, "mini")}
|
||||
alt={article.author?.first_name}
|
||||
class="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm font-medium">{article.author.first_name}</p>
|
||||
<p class="text-sm font-medium">{article.author?.first_name}</p>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="icon-[ri--calendar-line] w-4 h-4"></span>
|
||||
{timeAgo.format(new Date(article.publish_date))}
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Author Bio -->
|
||||
{#if data.article.author}
|
||||
<Card class="p-0 bg-gradient-to-r from-card/50 to-card">
|
||||
<CardContent class="p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
@@ -164,15 +165,13 @@
|
||||
>
|
||||
{data.article.author.website}
|
||||
</a>
|
||||
<!-- <a href="https://{data.article.author.social.website}" class="text-primary hover:underline">
|
||||
{data.article.author.social.website}
|
||||
</a> -->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<!-- Sidebar -->
|
||||
|
||||
Reference in New Issue
Block a user