fix tags crud

This commit is contained in:
Jordi Enric
2024-05-12 14:33:18 +02:00
parent 9c9619fcc0
commit f896b9715a
7 changed files with 298 additions and 77 deletions
@@ -44,7 +44,7 @@ export const useCreateBlogTag = () => {
},
{
onSuccess: () => {
queryClient.invalidateQueries(["tags"]);
queryClient.invalidateQueries(["blog-tags"]);
},
}
);
@@ -37,19 +37,19 @@ export function CreateTagDialog({ blogId }: PropsWithChildren<Props>) {
onSubmit={async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLFormElement;
const formData = new FormData(target);
const name = formData.get("name") as string;
const slug = formData.get("slug") as string;
if (!name || !slug) {
if (!title || !slug) {
toast.error("Name and slug are required");
return;
}
await createTag.mutateAsync({ name, slug, blog_id: blogId });
formData.set("name", "");
formData.set("slug", "");
await createTag.mutateAsync({
name: title,
slug,
blog_id: blogId,
});
setTitle("");
setSlug("");
target.reset();
toast.success("Tag created");
setOpen(false);
}}
@@ -3,18 +3,29 @@ import { Button } from "../ui/button";
import { Dialog, DialogContent } from "../ui/dialog";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { useEffect, useState } from "react";
type Props = {
tag: {
tag_id: string | null;
tag_name: string | null;
slug: string | null;
};
onSubmit: (tag: { tag_name: string; slug: string }) => void;
tag:
| {
tag_id: string | null;
tag_name: string | null;
slug: string | null;
}
| undefined;
onSubmit: (tag: { tag_name: string; slug: string; tag_id: string }) => void;
open: boolean;
onOpenChange: (value: boolean) => void;
};
export function UpdateTagDialog({ onSubmit, tag, open, onOpenChange }: Props) {
const [newTitle, setNewTitle] = useState(tag?.tag_name || "");
const [newSlug, setNewSlug] = useState(tag?.slug || "");
useEffect(() => {
setNewTitle(tag?.tag_name || "");
setNewSlug(tag?.slug || "");
}, [tag]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="md:max-w-sm">
@@ -24,26 +35,39 @@ export function UpdateTagDialog({ onSubmit, tag, open, onOpenChange }: Props) {
onSubmit={async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLFormElement;
const formData = new FormData(target);
const tag_name = formData.get("tag_name") as string;
const slug = formData.get("slug") as string;
if (!tag_name || !slug) {
toast.error("Name and slug are required");
if (!tag?.tag_id) {
return;
}
onSubmit({ tag_name, slug });
formData.set("name", "");
formData.set("slug", "");
onSubmit({
tag_name: newTitle,
slug: newSlug,
tag_id: tag?.tag_id,
});
target.reset();
toast.success("Tag updated");
setNewTitle("");
setNewSlug("");
}}
>
<Label>Name</Label>
<Input name="tag_name" required defaultValue={tag.tag_name!} />
<Input
name="tag_name"
required
onChange={(e) => {
setNewTitle(e.target.value);
}}
value={newTitle}
/>
<Label>Slug</Label>
<Input name="slug" required defaultValue={tag.slug!} />
<Input
name="slug"
required
value={newSlug}
onChange={(e) => {
// if the user writes a space, we should replace it with a dash
const value = e.target.value.replace(/\s/g, "-");
setNewSlug(value);
}}
/>
<div className="flex items-center justify-end gap-2">
<Button type="submit">Update</Button>
</div>
+37 -23
View File
@@ -86,7 +86,11 @@ export default function BlogPosts() {
});
const deleteMedia = useDeleteMediaMutation();
const [selectedImages, setSelectedImages] = useState<Image[]>([]);
const [selectedTag, setSelectedTag] = useState<{ tag_id?: string | null }>();
const [selectedTag, setSelectedTag] = useState<{
tag_id: string | null;
tag_name: string | null;
slug: string | null;
}>();
const tags = useTagsWithUsageQuery(
{ blogId },
@@ -118,8 +122,8 @@ export default function BlogPosts() {
},
});
const deleteTagMutation = useDeleteTagMutation();
const updateTagMutation = useUpdateTagMutation();
const deleteTagMutation = useDeleteTagMutation(blogId);
const updateTagMutation = useUpdateTagMutation(blogId);
function getPostViews(slug: string) {
const post = postViews.data?.find((p: any) => p.post_slug === slug);
@@ -378,7 +382,7 @@ export default function BlogPosts() {
<div className="grid grid-cols-4 items-center p-2 text-sm font-medium text-zinc-600">
<div>Tag</div>
<div>Slug</div>
<div>Posts with tag</div>
<div className="text-right">Posts with tag</div>
<div></div>
</div>
) : null}
@@ -400,7 +404,9 @@ export default function BlogPosts() {
</p>
</div>
<div className="font-mono">{tag.post_count}</div>
<div className="text-right font-mono">
{tag.post_count}
</div>
<div className="flex items-center justify-end">
<DropdownMenu modal={false}>
@@ -412,6 +418,10 @@ export default function BlogPosts() {
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => {
if (!tag) {
return;
}
setSelectedTag(tag);
setUpdateTagDialogOpen(true);
}}
>
@@ -420,7 +430,9 @@ export default function BlogPosts() {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
console.log("selected tag", tag);
if (!tag) {
return;
}
setSelectedTag(tag);
setDeleteTagDialogOpen(true);
}}
@@ -431,23 +443,6 @@ export default function BlogPosts() {
</DropdownMenuContent>
</DropdownMenu>
</div>
<UpdateTagDialog
tag={tag}
onSubmit={async (newTag) => {
const res = await updateTagMutation.mutateAsync({
name: newTag.tag_name,
slug: newTag.slug,
id: tag.tag_id!,
});
if (res.error) {
toast.error("Failed to update tag");
return;
}
toast.success("Tag updated");
}}
open={updateTagDialogOpen}
onOpenChange={setUpdateTagDialogOpen}
/>
</div>
);
})}
@@ -456,6 +451,25 @@ export default function BlogPosts() {
</TabSection>
</TabsContent>
</Tabs>
<UpdateTagDialog
tag={selectedTag}
onSubmit={async (newTag) => {
if (!newTag.tag_id) return;
const res = await updateTagMutation.mutateAsync({
name: newTag.tag_name,
slug: newTag.slug,
id: newTag.tag_id,
});
if (res.error) {
toast.error("Failed to update tag");
return;
}
setUpdateTagDialogOpen(false);
toast.success("Tag updated");
}}
open={updateTagDialogOpen}
onOpenChange={setUpdateTagDialogOpen}
/>
<ConfirmDialog
title="Delete tag"
description="Are you sure you want to delete this tag? This action cannot be undone."
+11 -7
View File
@@ -2,8 +2,8 @@ import { getSupabaseBrowserClient } from "@/lib/supabase";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export const tagKeys = {
tags: (keys: string[]) => ["tags", ...keys],
tag: (tagId: string) => ["tag", tagId],
tags: (keys: string[]) => ["blog-tags", ...keys],
tag: (tagId: string) => ["blog-tag", tagId],
};
export function useTagsWithUsageQuery(
@@ -30,13 +30,17 @@ export function useTagsWithUsageQuery(
});
}
export function useDeleteTagMutation() {
export function useDeleteTagMutation(blogId: string) {
const queryClient = useQueryClient();
const supa = getSupabaseBrowserClient();
return useMutation(
async (tagId: string) => {
const res = await supa.from("blog_tags").delete().eq("id", tagId);
const res = await supa
.from("blog_tags")
.delete()
.eq("id", tagId)
.eq("blog_id", blogId);
if (res.error) {
throw new Error(res.error.message);
@@ -46,13 +50,13 @@ export function useDeleteTagMutation() {
},
{
onSuccess: () => {
queryClient.invalidateQueries(tagKeys.tags([""]));
queryClient.invalidateQueries(tagKeys.tags([blogId]));
},
}
);
}
export function useUpdateTagMutation() {
export function useUpdateTagMutation(blogId: string) {
const queryClient = useQueryClient();
const supa = getSupabaseBrowserClient();
@@ -68,7 +72,7 @@ export function useUpdateTagMutation() {
},
{
onSuccess: () => {
queryClient.invalidateQueries(tagKeys.tags([""]));
queryClient.invalidateQueries(tagKeys.tags([blogId]));
},
}
);
+196 -18
View File
@@ -6,7 +6,7 @@ export type Json =
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
export type Database = {
public: {
Tables: {
blog_tags: {
@@ -41,9 +41,10 @@ export interface Database {
{
foreignKeyName: "blog_tags_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
blogs: {
@@ -52,7 +53,7 @@ export interface Database {
description: string | null
emoji: string
id: string
slug: string | null
slug: string
title: string
user_id: string
}
@@ -61,7 +62,7 @@ export interface Database {
description?: string | null
emoji: string
id?: string
slug?: string | null
slug: string
title: string
user_id?: string
}
@@ -70,7 +71,7 @@ export interface Database {
description?: string | null
emoji?: string
id?: string
slug?: string | null
slug?: string
title?: string
user_id?: string
}
@@ -78,15 +79,17 @@ export interface Database {
{
foreignKeyName: "blogs_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "blogs_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
}
},
]
}
feedback: {
@@ -157,61 +160,71 @@ export interface Database {
{
foreignKeyName: "post_tags_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts"
referencedColumns: ["id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_blog_and_subscription_status"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_blog_and_subscription_status_v2"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_tags"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_tags_v2"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "public_posts_v1"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_tag_id_fkey"
columns: ["tag_id"]
isOneToOne: false
referencedRelation: "blog_tags"
referencedColumns: ["id"]
},
{
foreignKeyName: "post_tags_tag_id_fkey"
columns: ["tag_id"]
referencedRelation: "tag_usage_count_v1"
isOneToOne: false
referencedRelation: "tag_usage_count_v2"
referencedColumns: ["tag_id"]
}
},
]
}
posts: {
Row: {
abstract: string
blog_id: string
content: Json
cover_image: string | null
@@ -227,6 +240,7 @@ export interface Database {
user_id: string
}
Insert: {
abstract?: string
blog_id: string
content?: Json
cover_image?: string | null
@@ -242,6 +256,7 @@ export interface Database {
user_id?: string
}
Update: {
abstract?: string
blog_id?: string
content?: Json
cover_image?: string | null
@@ -260,21 +275,24 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
},
{
foreignKeyName: "public_posts_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "public_posts_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
}
},
]
}
prices: {
@@ -345,15 +363,17 @@ export interface Database {
{
foreignKeyName: "subscriptions_user_id_fkey"
columns: ["user_id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_user_id_fkey"
columns: ["user_id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
}
},
]
}
teams: {
@@ -382,19 +402,90 @@ export interface Database {
{
foreignKeyName: "public_teams_owner_id_fkey"
columns: ["owner_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "public_teams_owner_id_fkey"
columns: ["owner_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
}
},
]
}
}
Views: {
post_tags_with_tags: {
Row: {
created_at: string | null
id: number | null
post_id: string | null
tag_description: string | null
tag_id: string | null
tag_name: string | null
tag_slug: string | null
}
Relationships: [
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts"
referencedColumns: ["id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_blog_and_subscription_status"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_blog_and_subscription_status_v2"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_tags"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "posts_with_tags_v2"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_post_id_fkey"
columns: ["post_id"]
isOneToOne: false
referencedRelation: "public_posts_v1"
referencedColumns: ["post_id"]
},
{
foreignKeyName: "post_tags_tag_id_fkey"
columns: ["tag_id"]
isOneToOne: false
referencedRelation: "blog_tags"
referencedColumns: ["id"]
},
{
foreignKeyName: "post_tags_tag_id_fkey"
columns: ["tag_id"]
isOneToOne: false
referencedRelation: "tag_usage_count_v2"
referencedColumns: ["tag_id"]
},
]
}
posts_with_blog_and_subscription_status: {
Row: {
blog_id: string | null
@@ -415,9 +506,10 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
posts_with_blog_and_subscription_status_v2: {
@@ -441,9 +533,10 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
posts_with_tags: {
@@ -465,9 +558,10 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
posts_with_tags_v2: {
@@ -490,9 +584,10 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
public_posts_v1: {
@@ -517,12 +612,13 @@ export interface Database {
{
foreignKeyName: "posts_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
tag_usage_count_v1: {
tag_usage_count_v2: {
Row: {
blog_id: string | null
created_at: string | null
@@ -536,9 +632,10 @@ export interface Database {
{
foreignKeyName: "blog_tags_blog_id_fkey"
columns: ["blog_id"]
isOneToOne: false
referencedRelation: "blogs"
referencedColumns: ["id"]
}
},
]
}
users: {
@@ -687,3 +784,84 @@ export interface Database {
}
}
type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables<
PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
PublicSchema["Views"])
? (PublicSchema["Tables"] &
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never
+1
View File
@@ -8,6 +8,7 @@
"start": "concurrently --names \"APP,STRIPE\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run dev\" \"npm run stripe:webhook\"",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"db:typegen": "npx supabase gen types typescript --local --schema public > apps/web/src/types/supabase.ts",
"db:typegen:remote": "npx supabase gen types typescript --project-id ppfseefimhneysnokffx --schema public > apps/web/src/types/supabase.ts",
"db:start": "npx supabase start",
"db:stop": "npx supabase stop",
"db:open": "open http://localhost:54323",