From 01de367bc4ebe7bb187f9a280e182469cd23cddb Mon Sep 17 00:00:00 2001 From: Alexandre Marques Date: Wed, 12 Nov 2025 18:51:43 +0000 Subject: [PATCH 1/4] feat: add NotificationsSideSection component and integrate into CompaniesList and SideBar - Introduced NotificationsSideSection to display weekly email digest and manage notifications. - Integrated NotificationsSideSection into CompaniesList and SideBar for enhanced user experience. - Updated settings management to utilize new SettingsTab enum for better type safety. --- apps/web/src/components/CompaniesList.tsx | 2 ++ .../components/NotificationsSideSection.tsx | 26 ++++++++++++++++ apps/web/src/components/SideBar.tsx | 3 +- apps/web/src/components/settings/index.tsx | 30 +++++++++++-------- apps/web/src/hooks/useSettingsTab.tsx | 10 +++++++ apps/web/src/lib/search-params.ts | 23 ++++++++++++++ apps/web/src/lib/types.ts | 6 ++-- 7 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/components/NotificationsSideSection.tsx create mode 100644 apps/web/src/hooks/useSettingsTab.tsx diff --git a/apps/web/src/components/CompaniesList.tsx b/apps/web/src/components/CompaniesList.tsx index ea4f5d2..ea663b6 100644 --- a/apps/web/src/components/CompaniesList.tsx +++ b/apps/web/src/components/CompaniesList.tsx @@ -9,6 +9,7 @@ import CompaniesListFooter from "./CompaniesListFooter"; import { CompaniesListHeader } from "./CompaniesListHeader"; import CompanyItem from "./CompanyItem"; import { EmptyState } from "./EmptyState"; +import { NotificationsSideSection } from "./NotificationsSideSection"; import SponsorSideSection from "./SponsorSideSection"; const PAGE_SIZE = 15; @@ -89,6 +90,7 @@ export default function CompaniesList({ )}
+
diff --git a/apps/web/src/components/NotificationsSideSection.tsx b/apps/web/src/components/NotificationsSideSection.tsx new file mode 100644 index 0000000..9ed85b2 --- /dev/null +++ b/apps/web/src/components/NotificationsSideSection.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; +import { Button } from "./ui/button"; +import { RetroContainer } from "./ui/retro-container"; + +export const NotificationsSideSection = () => { + return ( + +
+

+ Weekly Email Digest 📧 +

+

+ Never miss out on the latest tech companies in Portugal. +

+ +
+
+ ); +}; diff --git a/apps/web/src/components/SideBar.tsx b/apps/web/src/components/SideBar.tsx index 22d4858..e762f79 100644 --- a/apps/web/src/components/SideBar.tsx +++ b/apps/web/src/components/SideBar.tsx @@ -1,3 +1,4 @@ +import { NotificationsSideSection } from "./NotificationsSideSection"; import { SearchSideBar } from "./SearchSideBar"; import SponsorSideSection from "./SponsorSideSection"; @@ -9,8 +10,8 @@ type SideBarProps = { export function SideBar({ locationOptions, categoryOptions }: SideBarProps) { return ( ); diff --git a/apps/web/src/components/settings/index.tsx b/apps/web/src/components/settings/index.tsx index 1ae3a08..13e7cf8 100644 --- a/apps/web/src/components/settings/index.tsx +++ b/apps/web/src/components/settings/index.tsx @@ -1,32 +1,41 @@ "use client"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import type { SettingsTab } from "@/lib/types"; +import { SettingsTab, settingsQueryStateKeys } from "@/lib/search-params"; +import type { SettingsTabs } from "@/lib/types"; import { cn } from "@/lib/utils"; -import { trackEvent } from "@tech-companies-portugal/analytics/client"; +import { useQueryStates } from "nuqs"; import { Title } from "../Title"; import { AccountSettings } from "./account/AccountSettings"; import { NotificationSettings } from "./notification/NotificationSettings"; -const TABS: SettingsTab[] = [ +const TABS: SettingsTabs[] = [ { - id: "account", + id: SettingsTab.ACCOUNT, title: "Account", }, { - id: "notifications", + id: SettingsTab.NOTIFICATIONS, title: "Notifications", }, ]; export const Settings = () => { + const [settingsTab, setSettingsTab] = useQueryStates(settingsQueryStateKeys, { + scroll: true, + }); + return (
</div> - <Tabs defaultValue="account" className="bold"> + <Tabs + value={settingsTab.tab} + onValueChange={(value) => setSettingsTab({ tab: value as SettingsTab })} + className="bold" + > <TabsList className="bg-transparent flex justify-start gap-4 w-full mb-4 overflow-x-auto scrollbar-hide"> {TABS.map((tab) => ( <TabsTrigger @@ -38,18 +47,13 @@ export const Settings = () => { )} > {tab.title} - {tab.badge && tab.badge} </TabsTrigger> ))} </TabsList> - <TabsContent value="account" className="p-0.5"> + <TabsContent value={SettingsTab.ACCOUNT} className="p-0.5"> <AccountSettings /> </TabsContent> - <TabsContent - value="notifications" - className="p-0.5" - onClick={() => trackEvent("notifications_tab_clicked")} - > + <TabsContent value={SettingsTab.NOTIFICATIONS} className="p-0.5"> <NotificationSettings /> </TabsContent> </Tabs> diff --git a/apps/web/src/hooks/useSettingsTab.tsx b/apps/web/src/hooks/useSettingsTab.tsx new file mode 100644 index 0000000..8dda443 --- /dev/null +++ b/apps/web/src/hooks/useSettingsTab.tsx @@ -0,0 +1,10 @@ +import { settingsQueryStateKeys } from "@/lib/search-params"; +import { useQueryStates } from "nuqs"; + +export const useSettingsTab = () => { + const [settingsTab, setSettingsTab] = useQueryStates(settingsQueryStateKeys, { + scroll: true, + }); + + return { settingsTab, setSettingsTab }; +}; diff --git a/apps/web/src/lib/search-params.ts b/apps/web/src/lib/search-params.ts index 4b967a2..bb203e1 100644 --- a/apps/web/src/lib/search-params.ts +++ b/apps/web/src/lib/search-params.ts @@ -3,6 +3,7 @@ import { createSearchParamsCache, parseAsFloat, parseAsString, + parseAsStringEnum, } from "nuqs/server"; export const defaultSearchParams = { @@ -12,6 +13,15 @@ export const defaultSearchParams = { page: 1, }; +export enum SettingsTab { + ACCOUNT = "account", + NOTIFICATIONS = "notifications", +} + +export const defaultSettings = { + tab: SettingsTab.ACCOUNT, +}; + // can be used in the client as well export const searchParamsQueryStateKeys = { query: parseAsString.withDefault(defaultSearchParams.query), @@ -27,3 +37,16 @@ export const loadSearchParams = createLoader(searchParamsQueryStateKeys); export const searchParamsCache = createSearchParamsCache( searchParamsQueryStateKeys, ); + +// query state keys for the settings page +export const settingsQueryStateKeys = { + tab: parseAsStringEnum<SettingsTab>(Object.values(SettingsTab)).withDefault( + defaultSettings.tab, + ), +}; + +// for server side +export const loadSettings = createLoader(settingsQueryStateKeys); + +// For getting server side cached settings in nested components tree +export const settingsCache = createSearchParamsCache(settingsQueryStateKeys); diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 3f6a019..880a207 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -1,3 +1,5 @@ +import type { SettingsTab } from "./search-params"; + export type Company = { slug: string; name: string; @@ -27,8 +29,8 @@ export type PageViewsData = { export type NextParams<T> = Promise<T>; -export type SettingsTab = { - id: string; +export type SettingsTabs = { + id: SettingsTab; title: string; disabled?: boolean; badge?: React.ReactNode; From be9557837e9db3d17c9feb4862ffd64b0e6eb340 Mon Sep 17 00:00:00 2001 From: Alexandre Marques <pt.alexandremarques@gmail.com> Date: Wed, 12 Nov 2025 18:52:13 +0000 Subject: [PATCH 2/4] chore: remove unused useSettingsTab hook - Deleted the useSettingsTab hook as it is no longer needed in the project, streamlining the codebase. --- apps/web/src/hooks/useSettingsTab.tsx | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 apps/web/src/hooks/useSettingsTab.tsx diff --git a/apps/web/src/hooks/useSettingsTab.tsx b/apps/web/src/hooks/useSettingsTab.tsx deleted file mode 100644 index 8dda443..0000000 --- a/apps/web/src/hooks/useSettingsTab.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { settingsQueryStateKeys } from "@/lib/search-params"; -import { useQueryStates } from "nuqs"; - -export const useSettingsTab = () => { - const [settingsTab, setSettingsTab] = useQueryStates(settingsQueryStateKeys, { - scroll: true, - }); - - return { settingsTab, setSettingsTab }; -}; From 592eea92a6fe91369ef900e28a17303072d91dc1 Mon Sep 17 00:00:00 2001 From: Alexandre Marques <pt.alexandremarques@gmail.com> Date: Sat, 15 Nov 2025 14:40:04 +0000 Subject: [PATCH 3/4] refactor: remove SponsorSideSection and update NotificationsSideSection layout - Deleted SponsorSideSection from CompaniesList and SideBar components to streamline the UI. - Enhanced NotificationsSideSection layout for better user engagement, updating text and structure for clarity. --- apps/web/src/components/CompaniesList.tsx | 2 -- .../components/NotificationsSideSection.tsx | 21 ++++++++------- apps/web/src/components/SideBar.tsx | 2 -- .../web/src/components/SponsorSideSection.tsx | 27 ------------------- 4 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 apps/web/src/components/SponsorSideSection.tsx diff --git a/apps/web/src/components/CompaniesList.tsx b/apps/web/src/components/CompaniesList.tsx index ea663b6..1a2b6c1 100644 --- a/apps/web/src/components/CompaniesList.tsx +++ b/apps/web/src/components/CompaniesList.tsx @@ -10,7 +10,6 @@ import { CompaniesListHeader } from "./CompaniesListHeader"; import CompanyItem from "./CompanyItem"; import { EmptyState } from "./EmptyState"; import { NotificationsSideSection } from "./NotificationsSideSection"; -import SponsorSideSection from "./SponsorSideSection"; const PAGE_SIZE = 15; @@ -91,7 +90,6 @@ export default function CompaniesList({ )} <div className="block lg:hidden space-y-4"> <NotificationsSideSection /> - <SponsorSideSection /> </div> </> ); diff --git a/apps/web/src/components/NotificationsSideSection.tsx b/apps/web/src/components/NotificationsSideSection.tsx index 9ed85b2..1f2d7b4 100644 --- a/apps/web/src/components/NotificationsSideSection.tsx +++ b/apps/web/src/components/NotificationsSideSection.tsx @@ -1,3 +1,4 @@ +import { SettingsTab } from "@/lib/search-params"; import Link from "next/link"; import { Button } from "./ui/button"; import { RetroContainer } from "./ui/retro-container"; @@ -8,16 +9,16 @@ export const NotificationsSideSection = () => { variant="static-secondary" className="px-4 py-3 lg:w-[290px]" > - <div className="space-y-3 text-center"> - <h2 className="text-lg font-semibold flex items-center justify-center gap-2"> - Weekly Email Digest 📧 - </h2> - <p className="text-sm text-muted-foreground"> - Never miss out on the latest tech companies in Portugal. - </p> - <Button variant="default" size="sm" className="w-full" asChild> - <Link href="/settings?tab=notifications" prefetch> - Manage notifications + <div className="flex flex-wrap items-center justify-center w-full gap-2"> + <div className="flex-1 min-w-[180px]"> + <h2 className="text-lg font-semibold">Stay updated 🔔</h2> + <p className="text-xs text-muted-foreground"> + Get notified when new companies are added. + </p> + </div> + <Button variant="default" size="sm" className="px-2" asChild> + <Link href={`/settings?tab=${SettingsTab.NOTIFICATIONS}`} prefetch> + Subscribe now </Link> </Button> </div> diff --git a/apps/web/src/components/SideBar.tsx b/apps/web/src/components/SideBar.tsx index e762f79..ca35702 100644 --- a/apps/web/src/components/SideBar.tsx +++ b/apps/web/src/components/SideBar.tsx @@ -1,6 +1,5 @@ import { NotificationsSideSection } from "./NotificationsSideSection"; import { SearchSideBar } from "./SearchSideBar"; -import SponsorSideSection from "./SponsorSideSection"; type SideBarProps = { locationOptions: string[]; @@ -10,7 +9,6 @@ type SideBarProps = { export function SideBar({ locationOptions, categoryOptions }: SideBarProps) { return ( <aside className="h-fit shrink-0 flex-col gap-4 lg:sticky lg:top-[60px] lg:flex-col-reverse hidden lg:flex"> - <SponsorSideSection /> <NotificationsSideSection /> <SearchSideBar {...{ locationOptions, categoryOptions }} /> </aside> diff --git a/apps/web/src/components/SponsorSideSection.tsx b/apps/web/src/components/SponsorSideSection.tsx deleted file mode 100644 index 020852f..0000000 --- a/apps/web/src/components/SponsorSideSection.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { RetroContainer } from "./ui/retro-container"; - -export default function SponsorSideSection() { - return ( - <RetroContainer - variant="static-secondary" - className="px-4 py-3 lg:w-[290px]" - > - <div className="space-y-3 text-center"> - <h2 className="text-lg font-semibold flex items-center justify-center gap-2"> - Be a Sponsor 💛 - </h2> - <p className="text-sm tracking-wide text-gray-500"> - <a - href="https://x.com/alexlmarques" - target="_blank" - rel="noreferrer" - className="text-primary underline-offset-2 underline" - > - Contact us - </a>{" "} - to become a sponsor or advertiser. - </p> - </div> - </RetroContainer> - ); -} From a26c5c04539a6169b4cace8c04cbca0a155a598f Mon Sep 17 00:00:00 2001 From: Alexandre Marques <pt.alexandremarques@gmail.com> Date: Sat, 15 Nov 2025 15:10:28 +0000 Subject: [PATCH 4/4] feat: integrate Suspense in SettingsPage and update notification label - Wrapped the Settings component in Suspense for improved loading behavior. - Updated notification label for clarity in NotificationSettings component. --- apps/web/src/app/settings/page.tsx | 5 +++-- .../settings/notification/NotificationSettings.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 29e8256..e980b49 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -1,11 +1,12 @@ import { Settings } from "@/components/settings"; +import { Suspense } from "react"; export default function SettingsPage() { // let's keep this page here for SSR, possibly to prefech some data on the server side, suspense queries etc. return ( - <> + <Suspense fallback=""> <Settings /> - </> + </Suspense> ); } diff --git a/apps/web/src/components/settings/notification/NotificationSettings.tsx b/apps/web/src/components/settings/notification/NotificationSettings.tsx index 978668d..092a34c 100644 --- a/apps/web/src/components/settings/notification/NotificationSettings.tsx +++ b/apps/web/src/components/settings/notification/NotificationSettings.tsx @@ -12,7 +12,7 @@ import { memo, useCallback } from "react"; import { useThrottledCallback } from "use-debounce"; const NOTIFICATION_LABEL_MAP = { - new_companies: "Receive weekly email updates for new companies added", + new_companies: "Receive email updates for new companies added", } as const; export const NotificationSettings = () => {