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
7 changes: 7 additions & 0 deletions app/api/links/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { triggerPusherEvent } from "@/lib/pusher";
import { resolveActiveWorkspace } from "@/lib/workspace";

import { validatePlatformUrl, detectPlatform, slugifyPlatform, isKnownPlatform, type Platform } from "@/lib/platforms";
Expand Down Expand Up @@ -225,6 +226,8 @@ export async function PUT(
// The updated link may be rendered on the public profile — purge the cache.
await invalidateProfileCache(link.workspaceId);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: link.workspaceId });

return NextResponse.json({ success: true, link: updatedLink });
} catch (err: unknown) {
const error = err as { code?: string; proposedRoute?: string };
Expand Down Expand Up @@ -330,6 +333,8 @@ export async function DELETE(
// Deleted links disappear from the public profile — purge the cache.
await invalidateProfileCache(link.workspaceId);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: link.workspaceId });

return NextResponse.json({ success: true });
}

Expand All @@ -341,6 +346,8 @@ export async function DELETE(
// Deleted links disappear from the public profile — purge the cache.
await invalidateProfileCache(link.workspaceId);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: link.workspaceId });

return NextResponse.json({ success: true });
}

Expand Down
3 changes: 3 additions & 0 deletions app/api/links/reorder/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { triggerPusherEvent } from "@/lib/pusher";
import { resolveActiveWorkspace } from "@/lib/workspace";
import { invalidateProfileCache } from "@/lib/profileCache";

Expand Down Expand Up @@ -158,6 +159,8 @@ export async function POST(req: Request) {
// Link order is part of the public profile payload — purge the cache.
await invalidateProfileCache(workspace.id);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: workspace.id });

return NextResponse.json({ ok: true, changed: updates.length });
} catch (err) {
console.error("/api/links/reorder error", err);
Expand Down
5 changes: 5 additions & 0 deletions app/api/links/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { triggerPusherEvent } from "@/lib/pusher";
import { resolveActiveWorkspace } from "@/lib/workspace";

import {
Expand Down Expand Up @@ -83,6 +84,8 @@ export async function POST(req: NextRequest) {
// New link is public — purge the cached public profile.
await invalidateProfileCache(workspace.id);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: workspace.id });

return NextResponse.json({ link: { ...link, children: [] } });
} catch (err: unknown) {
const error = err as { code?: string };
Expand Down Expand Up @@ -242,6 +245,8 @@ export async function POST(req: NextRequest) {
// New link is public — purge the cached public profile.
await invalidateProfileCache(workspace.id);

await triggerPusherEvent(`private-user-${session.user.id}`, 'links-updated', { workspaceId: workspace.id });

return NextResponse.json({ link });
} catch (err: unknown) {
const error = err as { code?: string };
Expand Down
34 changes: 34 additions & 0 deletions app/api/pusher/auth/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { pusherServer } from '@/lib/pusher';

export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions);

if (!session?.user?.id) {
return new NextResponse('Unauthorized', { status: 401 });
}

const data = await req.text();
const params = new URLSearchParams(data);
const socketId = params.get('socket_id');
const channelName = params.get('channel_name');

if (!socketId || !channelName) {
return new NextResponse('Missing socket_id or channel_name', { status: 400 });
}

// Ensure users can only subscribe to their own private channel
if (channelName !== `private-user-${session.user.id}`) {
return new NextResponse('Forbidden', { status: 403 });
}

const authResponse = pusherServer.authorizeChannel(socketId, channelName);
return NextResponse.json(authResponse);
} catch (error) {
console.error('Pusher auth error:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
31 changes: 30 additions & 1 deletion app/dashboard/DashboardClient.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getPusherClient } from "@/lib/pusher";
import { DashboardNavbar } from "@/app/components/DashboardNavbar";
import { getCsrfToken } from "@/lib/csrfClient";
import toast, { Toaster } from "react-hot-toast";
Expand All @@ -14,6 +16,7 @@ import { LayoutStyle } from "@/app/[username]/types/type";
import { LivePreview } from "@/components/dashboard/LivePreview";

export default function DashboardClient({
userId,
workspaceId,
username,
initialLinks,
Expand All @@ -33,6 +36,7 @@ export default function DashboardClient({
initialThemeColor,
initialThemeCustom,
}: {
userId?: string;
workspaceId: string;
username: string;
initialLinks: ProfileLink[];
Expand All @@ -52,7 +56,32 @@ export default function DashboardClient({
initialThemeColor?: string;
initialThemeCustom?: string | null;
}) {
const router = useRouter();
const [links, setLinks] = useState(initialLinks);

useEffect(() => {
setLinks(initialLinks);
}, [initialLinks]);

useEffect(() => {
if (!userId) return;

const pusher = getPusherClient();
if (!pusher) return;

const channelName = `private-user-${userId}`;
const channel = pusher.subscribe(channelName);

channel.bind('links-updated', () => {
router.refresh();
});

return () => {
channel.unbind_all();
pusher.unsubscribe(channelName);
};
}, [userId, router]);

const [theme, setTheme] = useState(initialTheme || "default");
const [layoutStyle, setLayoutStyle] = useState<LayoutStyle>(initialLayout || "LIST");
const [backgroundImage, setBackgroundImage] = useState<string | null>(initialBackgroundImage || "");
Expand Down
1 change: 1 addition & 0 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default async function DashboardPage() {

return (
<DashboardClient
userId={session.user.id}
workspaceId={workspace.id}
username={workspace.username}
initialLinks={nestedLinks}
Expand Down
36 changes: 36 additions & 0 deletions lib/pusher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import PusherServer from 'pusher';
import PusherClient from 'pusher-js';

export const pusherServer = new PusherServer({
appId: process.env.PUSHER_APP_ID || 'dummy_app_id',
key: process.env.NEXT_PUBLIC_PUSHER_KEY || 'dummy_key',
secret: process.env.PUSHER_SECRET || 'dummy_secret',
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER || 'mt1',
useTLS: true,
});

export const triggerPusherEvent = async (channel: string, event: string, data: any) => {
if (!process.env.PUSHER_APP_ID || !process.env.PUSHER_SECRET || !process.env.NEXT_PUBLIC_PUSHER_KEY) {
return;
}
try {
await pusherServer.trigger(channel, event, data);
} catch (err) {
console.error('Failed to trigger Pusher event:', err);
}
};

let clientInstance: PusherClient | null = null;

export const getPusherClient = () => {
if (!clientInstance && typeof window !== 'undefined') {
const key = process.env.NEXT_PUBLIC_PUSHER_KEY;
if (!key) return null;

clientInstance = new PusherClient(key, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER || 'mt1',
authEndpoint: '/api/pusher/auth',
});
}
return clientInstance;
};
Loading
Loading