diff --git a/apps/web/src/components/Editor/Editor.queries.ts b/apps/web/src/components/Editor/Editor.queries.ts index 0cf2742..b3e48c2 100644 --- a/apps/web/src/components/Editor/Editor.queries.ts +++ b/apps/web/src/components/Editor/Editor.queries.ts @@ -44,7 +44,7 @@ export const useCreateBlogTag = () => { }, { onSuccess: () => { - queryClient.invalidateQueries(["tags"]); + queryClient.invalidateQueries(["blog-tags"]); }, } ); diff --git a/apps/web/src/components/Tags/CreateTagDialog.tsx b/apps/web/src/components/Tags/CreateTagDialog.tsx index d54b630..25d55d7 100644 --- a/apps/web/src/components/Tags/CreateTagDialog.tsx +++ b/apps/web/src/components/Tags/CreateTagDialog.tsx @@ -37,19 +37,19 @@ export function CreateTagDialog({ blogId }: PropsWithChildren) { 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); }} diff --git a/apps/web/src/components/Tags/UpdateTagDialog.tsx b/apps/web/src/components/Tags/UpdateTagDialog.tsx index dece635..334f3d0 100644 --- a/apps/web/src/components/Tags/UpdateTagDialog.tsx +++ b/apps/web/src/components/Tags/UpdateTagDialog.tsx @@ -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 ( @@ -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(""); }} > - + { + setNewTitle(e.target.value); + }} + value={newTitle} + /> - + { + // if the user writes a space, we should replace it with a dash + const value = e.target.value.replace(/\s/g, "-"); + setNewSlug(value); + }} + />
diff --git a/apps/web/src/pages/blogs/[blogId]/posts.tsx b/apps/web/src/pages/blogs/[blogId]/posts.tsx index 180555b..6e73ee8 100644 --- a/apps/web/src/pages/blogs/[blogId]/posts.tsx +++ b/apps/web/src/pages/blogs/[blogId]/posts.tsx @@ -86,7 +86,11 @@ export default function BlogPosts() { }); const deleteMedia = useDeleteMediaMutation(); const [selectedImages, setSelectedImages] = useState([]); - 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() {
Tag
Slug
-
Posts with tag
+
Posts with tag
) : null} @@ -400,7 +404,9 @@ export default function BlogPosts() {

-
{tag.post_count}
+
+ {tag.post_count} +
@@ -412,6 +418,10 @@ export default function BlogPosts() { { + if (!tag) { + return; + } + setSelectedTag(tag); setUpdateTagDialogOpen(true); }} > @@ -420,7 +430,9 @@ export default function BlogPosts() { { - console.log("selected tag", tag); + if (!tag) { + return; + } setSelectedTag(tag); setDeleteTagDialogOpen(true); }} @@ -431,23 +443,6 @@ export default function BlogPosts() {
- { - 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} - /> ); })} @@ -456,6 +451,25 @@ export default function BlogPosts() { + { + 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} + /> ["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])); }, } ); diff --git a/apps/web/src/types/supabase.ts b/apps/web/src/types/supabase.ts index 149017f..1f1d057 100644 --- a/apps/web/src/types/supabase.ts +++ b/apps/web/src/types/supabase.ts @@ -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] + +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 diff --git a/package.json b/package.json index 0fc9d7a..e7b3b56 100644 --- a/package.json +++ b/package.json @@ -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",