From 7edbb9283cdd740d715f8dfb5fbfe479db845450 Mon Sep 17 00:00:00 2001 From: rimar1337 <132627503+rimar1337@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:42:21 +0700 Subject: [PATCH] sonner toasts --- package-lock.json | 10 ++ package.json | 1 + src/auto-imports.d.ts | 1 + src/providers/LikeMutationQueueProvider.tsx | 6 + src/routes/__root.tsx | 146 ++++++++++++++++++-- src/routes/profile.$did/index.tsx | 138 ++++++++++++------ 6 files changed, 255 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4d9cbb..e492c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-player": "^3.3.2", + "sonner": "^2.0.7", "tailwindcss": "^4.0.6", "tanstack-router-keepalive": "^1.0.0" }, @@ -12545,6 +12546,15 @@ "seroval-plugins": "~1.3.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", diff --git a/package.json b/package.json index fa8d10c..2637c0b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-player": "^3.3.2", + "sonner": "^2.0.7", "tailwindcss": "^4.0.6", "tanstack-router-keepalive": "^1.0.0" }, diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 3ca6a1f..c554833 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -20,6 +20,7 @@ declare global { const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default + const IconMdiClose: typeof import('~icons/mdi/close.jsx').default const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default } diff --git a/src/providers/LikeMutationQueueProvider.tsx b/src/providers/LikeMutationQueueProvider.tsx index a66712b..bf20f38 100644 --- a/src/providers/LikeMutationQueueProvider.tsx +++ b/src/providers/LikeMutationQueueProvider.tsx @@ -5,6 +5,7 @@ import { useAtom } from "jotai"; import React, { createContext, use, useCallback, useEffect, useRef } from "react"; import { useAuth } from "~/providers/UnifiedAuthProvider"; +import { renderSnack } from "~/routes/__root"; import { constellationURLAtom, internalLikedPostsAtom } from "~/utils/atoms"; import { constructArbitraryQuery, constructConstellationQuery, type linksRecordsResponse } from "~/utils/useQuery"; @@ -125,6 +126,11 @@ export function LikeMutationQueueProvider({ children }: { children: React.ReactN } } catch (err) { console.error("Like mutation failed, reverting:", err); + renderSnack({ + title: 'Like Mutation Failed', + description: 'Please try again.', + //button: { label: 'Try Again', onClick: () => console.log('whatever') }, + }) if (mutation.type === 'like') { setFastState(mutation.target, null); } else if (mutation.type === 'unlike') { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 074b5ba..562bdeb 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -14,6 +14,8 @@ import { import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { useAtom } from "jotai"; import * as React from "react"; +import { toast as sonnerToast } from "sonner"; +import { Toaster } from "sonner"; import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; import { Composer } from "~/components/Composer"; @@ -83,6 +85,7 @@ function RootComponent() { + @@ -91,6 +94,110 @@ function RootComponent() { ); } +export function AppToaster() { + return ( + + ); +} + +export function renderSnack({ + title, + description, + button, +}: Omit) { + return sonnerToast.custom((id) => ( + { + button?.onClick?.(); + }, + } + : undefined + } + /> + )); +} + +function Snack(props: ToastProps) { + const { title, description, button, id } = props; + + return ( +
+
+

{title}

+ {description ? ( +

+ {description} +

+ ) : null} +
+ + {button ? ( +
+ +
+ ) : null} + +
+ ); +} + +/* Types */ +interface ToastProps { + id: string | number; + title: string; + description?: string; + button?: { + label: string; + onClick: () => void; + }; +} + function RootDocument({ children }: { children: React.ReactNode }) { useAtomCssVar(hueAtom, "--tw-gray-hue"); const location = useLocation(); @@ -134,7 +241,13 @@ function RootDocument({ children }: { children: React.ReactNode }) {
@@ -485,7 +604,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { @@ -165,9 +179,18 @@ function ProfileComponent() { {handle}
- {followercount && ({followercount})}Followers + + {followercount && ( + + {followercount} + + )} + Followers + - - Follows + + Follows +
{description && (
@@ -212,23 +235,29 @@ function ProfileComponent() { } export type ProfilePostsFilter = { - posts: boolean, - replies: boolean, - mediaOnly: boolean, -} + posts: boolean; + replies: boolean; + mediaOnly: boolean; +}; export const defaultProfilePostsFilter: ProfilePostsFilter = { posts: true, replies: true, mediaOnly: false, -} +}; -function ProfilePostsFilterChipBar({filters, toggle}:{filters: ProfilePostsFilter | null, toggle: (key: keyof ProfilePostsFilter) => void}) { - const empty = (!filters?.replies && !filters?.posts); - const almostEmpty = (!filters?.replies && filters?.posts); +function ProfilePostsFilterChipBar({ + filters, + toggle, +}: { + filters: ProfilePostsFilter | null; + toggle: (key: keyof ProfilePostsFilter) => void; +}) { + const empty = !filters?.replies && !filters?.posts; + const almostEmpty = !filters?.replies && filters?.posts; useEffect(() => { if (empty) { - toggle("posts") + toggle("posts"); } }, [empty, toggle]); @@ -237,7 +266,7 @@ function ProfilePostsFilterChipBar({filters, toggle}:{filters: ProfilePostsFilte almostEmpty ? null : toggle("posts")} + onClick={() => (almostEmpty ? null : toggle("posts"))} /> { - setFilterses((prev)=>{ - return{ + setFilterses((prev) => { + return { ...prev, - [did]: obj - } - }) - } - useEffect(()=>{ + [did]: obj, + }; + }); + }; + useEffect(() => { if (!filters) { setFilters(defaultProfilePostsFilter); } - }) + }); useReusableTabScrollRestore(`Profile` + did); const queryClient = useQueryClient(); const { @@ -306,8 +335,12 @@ function PostsTab({ did }: { did: string }) { ); const toggle = (key: keyof ProfilePostsFilter) => { - setFilterses(prev => { - const existing = prev[did] ?? { posts: false, replies: false, mediaOnly: false }; // default + setFilterses((prev) => { + const existing = prev[did] ?? { + posts: false, + replies: false, + mediaOnly: false, + }; // default return { ...prev, @@ -805,7 +838,16 @@ export function FollowButton({ )} ) : ( - )} @@ -822,14 +864,14 @@ export function BiteButton({ const { data: identity } = useQueryIdentity(targetdidorhandle); const [show] = useAtom(enableBitesAtom); - if (!show) return + if (!show) return; return ( <>