diff --git a/bun.lockb b/bun.lockb index 1f60dbc..18f8d67 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/locales/en.json b/locales/en.json index 88805a1..63b4456 100644 --- a/locales/en.json +++ b/locales/en.json @@ -252,19 +252,7 @@ "cancel": "Cancel", "no-reviews": "No reviews yet. Add your first client review.", "count": "{count}/10 reviews", - "confirm-delete": "Are you sure you want to delete this review?", - "google-title": "Auto-pull Google Reviews", - "google-description": "Sync reviews from your Google Business Profile. Synced reviews appear alongside your manual reviews.", - "google-place-placeholder": "Google Place ID (e.g. ChIJN1t_tDeuEmsRUsoyG83frY4)", - "google-save": "Save", - "google-sync": "Sync", - "google-enter-place-id": "Enter your Google Place ID first", - "google-synced": "Synced {count} Google {count, plural, one {review} other {reviews}}", - "google-sync-failed": "Sync failed", - "google-last-synced": "Last synced: {date}", - "google-show-on-page": "Show Google reviews on my public page", - "google-find-place-id": "Google Place ID Finder", - "google-find-hint": "Find your Place ID at" + "confirm-delete": "Are you sure you want to delete this review?" }, "dashboard-themes": { "title": "Appearance", @@ -855,7 +843,6 @@ "login-success": "Login successful", "register-success": "Registration successful", "home": "Home", - "google-disconnected": "Google successfully disconnected", "not-found": "Not found", "username-required": "A username is required to publish your page", "username-reserved": "This username is reserved", @@ -868,7 +855,8 @@ "no-subscription": "No subscription found", "max-links": "Maximum 10 links allowed", "max-image-size": "Images must be maximum 5MB", - "image-type": "Images must be JPG, PNG or WebP" + "image-type": "Images must be JPG, PNG or WebP", + "google-disconnected": "Google successfully disconnected" }, "footer": { "tagline": "The professional page your clients expect.", diff --git a/package.json b/package.json index fc52d23..8082b8e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@tailwindcss/typography": "^0.5.19", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^1.6.0", + "@vercel/speed-insights": "^2.0.0", "arctic": "^3.7.0", "autoprefixer": "^10.4.24", "axios": "^1.13.5", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c056ef5..ce0b05e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,9 +44,6 @@ model User { calendlyUrl String? calendlyEnabled Boolean @default(false) zillowUrl String? - googlePlaceId String? - googleSyncEnabled Boolean @default(false) - googleReviewsLastSynced DateTime? zapierWebhookUrl String? fubApiKey String? mailchimpApiKey String? @@ -131,8 +128,6 @@ model Review { quote String stars Int @default(5) position Int @default(0) - isGoogleReview Boolean @default(false) - googleReviewId String? @unique userId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/app/[locale]/(dashboard)/dashboard/reviews/google-reviews-sync.tsx b/src/app/[locale]/(dashboard)/dashboard/reviews/google-reviews-sync.tsx deleted file mode 100644 index b90d709..0000000 --- a/src/app/[locale]/(dashboard)/dashboard/reviews/google-reviews-sync.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { RefreshCw, CheckCircle2, Star } from "lucide-react"; -import toast from "react-hot-toast"; -import { useTranslations } from "next-intl"; - -type GoogleReviewsSyncProps = { - googlePlaceId: string; - googleSyncEnabled: boolean; - lastSynced: Date | null; -}; - -const GoogleReviewsSync: React.FC = ({ - googlePlaceId: initialPlaceId, - googleSyncEnabled: initialEnabled, - lastSynced, -}) => { - const t = useTranslations("pages.dashboard-reviews"); - const tCommon = useTranslations("common"); - const [placeId, setPlaceId] = useState(initialPlaceId); - const [enabled, setEnabled] = useState(initialEnabled); - const [syncing, setSyncing] = useState(false); - const [saving, setSaving] = useState(false); - - const handleSave = async () => { - setSaving(true); - try { - const res = await fetch("/api/dashboard/reviews/google-sync", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - googlePlaceId: placeId, - googleSyncEnabled: enabled, - }), - }); - const data = await res.json(); - if (data.success) toast.success(tCommon("saved")); - else toast.error(data.message || "Error"); - } catch { - toast.error(tCommon("error-generic")); - } finally { - setSaving(false); - } - }; - - const handleSync = async () => { - if (!placeId) { - toast.error(t("google-enter-place-id")); - return; - } - setSyncing(true); - try { - const res = await fetch("/api/dashboard/reviews/google-sync", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ googlePlaceId: placeId }), - }); - const data = await res.json(); - if (data.success) { - toast.success(t("google-synced", { count: data.synced })); - } else { - toast.error(data.message || t("google-sync-failed")); - } - } catch { - toast.error(tCommon("error-generic")); - } finally { - setSyncing(false); - } - }; - - return ( -
-
-
- - - -
-

{t("google-title")}

- - PRO - -
-

- {t("google-description")} -

- -
- setPlaceId(e.target.value)} - /> -
- - -
- -
- -
- - {lastSynced && ( -

- - {t("google-last-synced", { - date: new Date(lastSynced).toLocaleString(), - })} -

- )} - -

- {t("google-find-hint")}{" "} - - {t("google-find-place-id")} - -

-
-
- ); -}; - -export default GoogleReviewsSync; diff --git a/src/app/[locale]/(dashboard)/dashboard/reviews/page.tsx b/src/app/[locale]/(dashboard)/dashboard/reviews/page.tsx index c82141e..7b609a8 100644 --- a/src/app/[locale]/(dashboard)/dashboard/reviews/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/reviews/page.tsx @@ -2,10 +2,8 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getUser } from "@/lib/auth"; -import { isPro } from "@/lib/subscription"; import db from "@/lib/db"; import { ReviewsManager } from "./reviews"; -import GoogleReviewsSync from "./google-reviews-sync"; export const generateMetadata = async (): Promise => { const t = await getTranslations("pages.dashboard-reviews"); @@ -20,21 +18,10 @@ const Page = async () => { redirect("/login"); } - const [reviews, pro, user] = await Promise.all([ - db.review.findMany({ - where: { userId: session.user.id }, - orderBy: { position: "asc" }, - }), - isPro(session.user.id), - db.user.findUnique({ - where: { id: session.user.id }, - select: { - googlePlaceId: true, - googleSyncEnabled: true, - googleReviewsLastSynced: true, - }, - }), - ]); + const reviews = await db.review.findMany({ + where: { userId: session.user.id }, + orderBy: { position: "asc" }, + }); return ( <> @@ -42,14 +29,6 @@ const Page = async () => {

{t("title")}

{t("description")}

- {pro && ( - - )} - diff --git a/src/app/api/dashboard/reviews/google-sync/route.ts b/src/app/api/dashboard/reviews/google-sync/route.ts deleted file mode 100644 index ba35d41..0000000 --- a/src/app/api/dashboard/reviews/google-sync/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { getUser } from "@/lib/auth"; -import db from "@/lib/db"; - -export const POST = async (req: NextRequest) => { - const user = await getUser(); - if (!user?.user) { - return NextResponse.json( - { success: false, message: "Unauthorized" }, - { status: 401 } - ); - } - - try { - const body = await req.json(); - const { googlePlaceId } = body; - - if (!googlePlaceId) { - return NextResponse.json({ - success: false, - message: "Place ID is required", - }); - } - - const apiKey = process.env.GOOGLE_PLACES_API_KEY; - if (!apiKey) { - return NextResponse.json({ - success: false, - message: "Google Places API is not configured on this server.", - }); - } - - // Fetch reviews from Google Places API - const response = await fetch( - `https://maps.googleapis.com/maps/api/place/details/json?place_id=${googlePlaceId}&fields=reviews&key=${apiKey}` - ); - const data = await response.json(); - - if (data.status !== "OK") { - return NextResponse.json({ - success: false, - message: `Google API error: ${data.status}`, - }); - } - - const reviews: Array<{ - author_name: string; - text: string; - rating: number; - time: number; - }> = data.result?.reviews ?? []; - - let synced = 0; - for (const r of reviews) { - const googleReviewId = `${googlePlaceId}_${r.author_name}_${r.time}`; - await db.review.upsert({ - where: { googleReviewId }, - update: {}, - create: { - name: r.author_name, - quote: r.text || "", - stars: Math.round(r.rating), - isGoogleReview: true, - googleReviewId, - userId: user.user.id, - }, - }); - synced++; - } - - await db.user.update({ - where: { id: user.user.id }, - data: { - googlePlaceId, - googleReviewsLastSynced: new Date(), - }, - }); - - return NextResponse.json({ success: true, synced }); - } catch (err) { - console.error(err); - return NextResponse.json({ success: false, message: "Server error" }); - } -}; - -export const PUT = async (req: NextRequest) => { - const user = await getUser(); - if (!user?.user) { - return NextResponse.json( - { success: false, message: "Unauthorized" }, - { status: 401 } - ); - } - - try { - const body = await req.json(); - const { googlePlaceId, googleSyncEnabled } = body; - - await db.user.update({ - where: { id: user.user.id }, - data: { - ...(googlePlaceId !== undefined && { - googlePlaceId: googlePlaceId || null, - }), - ...(googleSyncEnabled !== undefined && { googleSyncEnabled }), - }, - }); - - return NextResponse.json({ success: true }); - } catch (err) { - console.error(err); - return NextResponse.json({ success: false, message: "Server error" }); - } -}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3a097a7..a13d9fd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Analytics } from "@vercel/analytics/react"; +import { SpeedInsights } from "@vercel/speed-insights/next"; import { Poppins } from "next/font/google"; import { NextIntlClientProvider } from "next-intl"; import { getLocale, getMessages, getTranslations } from "next-intl/server"; @@ -67,6 +68,7 @@ const Root = async ({ children }: React.PropsWithChildren) => { {children} + );