Skip to content

Commit c76a54d

Browse files
committed
feat(premium): add "Remove Ads" feature with localization support and integrate into the header component
1 parent 47eb764 commit c76a54d

8 files changed

Lines changed: 155 additions & 117 deletions

File tree

locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,7 @@ export default {
18131813
subscription: "Subscription",
18141814
manage_subscription: "Manage subscription",
18151815
become_premium: "Become Premium",
1816+
remove_ads: "Remove Ads",
18161817
extremely_dissatisfied: "Extremely dissatisfied",
18171818
somewhat_dissatisfied: "Somewhat dissatisfied",
18181819
neutral: "Neutral",

locales/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ export default {
949949
subscription: "Suscripción",
950950
manage_subscription: "Gestionar suscripción",
951951
become_premium: "Torne-se Premium",
952+
remove_ads: "Eliminar anuncios",
952953
in_progress: "En progreso",
953954
close: "Cerrar",
954955
premium: "Premium",

locales/fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,7 @@ export default {
18391839
subscription: "Abonnement",
18401840
manage_subscription: "Gérer abonnement",
18411841
become_premium: "Devenir Premium",
1842+
remove_ads: "Supprimer les pubs",
18421843
coming_soon: "Bientôt disponible",
18431844
extremely_dissatisfied: "Très insatisfait",
18441845
somewhat_dissatisfied: "Insatisfait",

locales/pt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,7 @@ export default {
18251825
subscription: "Abonamento",
18261826
manage_subscription: "Gerir abonamento",
18271827
become_premium: "Torne-se Premium",
1828+
remove_ads: "Remover anúncios",
18281829
coming_soon: "Em breve",
18291830
free: "Gratuito",
18301831
new: "Novo",

locales/ru.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,7 @@ export default {
18151815
subscription: "Подписка",
18161816
manage_subscription: "Управление подпиской",
18171817
become_premium: "Стань Премиум",
1818+
remove_ads: "Убрать рекламу",
18181819
coming_soon: "Скоро",
18191820
premium: "Премиум",
18201821
free: "Бесплатно",

locales/zh-CN.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@ export default {
944944
subscription: "订阅",
945945
manage_subscription: "管理订阅",
946946
become_premium: "成为高级",
947+
remove_ads: "移除广告",
947948
coming_soon: "即将推出",
948949
in_progress: "进行中",
949950
close: "关闭",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { Ban } from "lucide-react";
5+
6+
import { useI18n } from "locales/client";
7+
8+
export function RemoveAdsText() {
9+
const router = useRouter();
10+
const t = useI18n();
11+
12+
const handleClick = () => {
13+
router.push("/premium");
14+
};
15+
16+
return (
17+
<button
18+
className="flex items-center gap-0 text-[11px] sm:text-xs md:font-medium text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-full transition-all duration-200 hover:scale-105 whitespace-nowrap hover:underline"
19+
onClick={handleClick}
20+
>
21+
<Ban className="w-3 h-3 flex-shrink-0 mr-1" />
22+
<span className="whitespace-nowrap">{t("commons.remove_ads")}</span>
23+
</button>
24+
);
25+
}

src/features/layout/Header.tsx

Lines changed: 124 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import Image from "next/image";
4-
import { LogIn, UserPlus, LogOut, User, Crown, CreditCard } from "lucide-react";
4+
import { LogIn, UserPlus, LogOut, User, Crown, Sparkles } from "lucide-react";
55

66
import { useI18n } from "locales/client";
77
import Logo from "@public/logo.png";
@@ -12,7 +12,9 @@ import { ReleaseNotesDialog } from "@/features/release-notes";
1212
import WorkoutStreakHeader from "@/features/layout/workout-streak-header";
1313
import { useLogout } from "@/features/auth/model/useLogout";
1414
import { useSession } from "@/features/auth/lib/auth-client";
15+
import { env } from "@/env";
1516
import { Link } from "@/components/ui/link";
17+
import { RemoveAdsText } from "@/components/premium/RemoveAdsText";
1618

1719
export const Header = () => {
1820
const session = useSession();
@@ -22,7 +24,10 @@ export const Header = () => {
2224

2325
// Get user initials for avatar
2426
const userAvatar = session.data?.user?.email?.substring(0, 2).toUpperCase() || "";
27+
2528
const isPremium = premiumStatus?.isPremium ?? false;
29+
const showAds = env.NEXT_PUBLIC_SHOW_ADS === true;
30+
const hasAdProvider = env.NEXT_PUBLIC_AD_CLIENT || env.NEXT_PUBLIC_AD_PROVIDER === "ezoic";
2631

2732
const handleSignOut = () => {
2833
logout.mutate();
@@ -35,134 +40,136 @@ export const Header = () => {
3540
};
3641

3742
return (
38-
<div className="navbar bg-base-100 dark:bg-black dark:text-gray-200 px-2 sm:px-4 rounded-tl-lg rounded-tr-lg">
39-
{/* Logo and Title */}
40-
<div className="navbar-start flex items-center gap-2">
41-
<Link
42-
className="group flex items-center space-x-3 rounded-xl bg-gradient-to-r px-2 sm:px-4 py-2 transition-all duration-200 dark:text-gray-200 dark:bg-gray-800"
43-
href="/"
44-
>
45-
<div className="relative flex-none">
46-
<Image
47-
alt="workout cool logo"
48-
className="h-10 w-10 sm:h-8 sm:w-8 transition-transform duration-200 group-hover:rotate-[20deg] group-hover:scale-110"
49-
height={32}
50-
priority
51-
src={Logo}
52-
width={32}
53-
/>
54-
<div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-emerald-400 opacity-0 transition-opacity duration-200 group-hover:opacity-100"></div>
55-
</div>
56-
<div className="flex-col hidden sm:flex">
57-
<span className="font-bold transition-colors duration-200 group-hover:text-blue-400">Workout.cool</span>
58-
</div>
59-
</Link>
60-
</div>
43+
<>
44+
<div className="navbar bg-base-100 dark:bg-black dark:text-gray-200 px-2 sm:px-4 rounded-tl-lg rounded-tr-lg">
45+
{/* Logo and Title */}
46+
<div className="navbar-start flex items-center gap-2">
47+
<Link
48+
className="group flex items-center space-x-3 rounded-xl bg-gradient-to-r px-2 sm:px-4 py-2 transition-all duration-200 dark:text-gray-200 dark:bg-gray-800"
49+
href="/"
50+
>
51+
<div className="relative flex-none">
52+
<Image
53+
alt="workout cool logo"
54+
className="h-10 w-10 sm:h-8 sm:w-8 transition-transform duration-200 group-hover:rotate-[20deg] group-hover:scale-110"
55+
height={32}
56+
priority
57+
src={Logo}
58+
width={32}
59+
/>
60+
<div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-emerald-400 opacity-0 transition-opacity duration-200 group-hover:opacity-100"></div>
61+
</div>
62+
<div className="flex-col hidden sm:flex">
63+
<span className="font-bold transition-colors duration-200 group-hover:text-blue-400">Workout.cool</span>
64+
</div>
65+
</Link>
66+
</div>
6167

62-
{/* User Menu */}
63-
<div className="navbar-end">
64-
<WorkoutStreakHeader />
65-
<ReleaseNotesDialog />
66-
<ThemeToggle />
67-
<LanguageSelector />
68+
{/* User Menu */}
69+
<div className="navbar-end">
70+
{isPremium || !showAds || !hasAdProvider ? <WorkoutStreakHeader /> : <RemoveAdsText />}
71+
<ReleaseNotesDialog />
72+
<ThemeToggle />
73+
<LanguageSelector />
6874

69-
<div className="dropdown dropdown-end ml-1">
70-
<div className="tooltip tooltip-bottom" data-tip={t("commons.profile")}>
71-
<div className="btn btn-ghost btn-circle avatar relative" role="button" tabIndex={0}>
72-
<div className="w-8 rounded-full bg-primary text-primary-content !flex items-center justify-center text-sm font-medium">
73-
{userAvatar || <User className="w-4 h-4" />}
74-
</div>
75-
{isPremium && (
76-
<div className="absolute -top-1 -right-1 w-4 h-4 bg-amber-400 rounded-full !flex items-center justify-center">
77-
<Crown className="w-2.5 h-2.5 text-amber-900" />
75+
<div className="dropdown dropdown-end ml-1">
76+
<div className="tooltip tooltip-bottom" data-tip={t("commons.profile")}>
77+
<div className="btn btn-ghost btn-circle avatar relative" role="button" tabIndex={0}>
78+
<div className="w-8 rounded-full bg-primary text-primary-content !flex items-center justify-center text-sm font-medium">
79+
{userAvatar || <User className="w-4 h-4" />}
7880
</div>
79-
)}
81+
{isPremium && (
82+
<div className="absolute -top-1 -right-1 w-4 h-4 bg-amber-400 rounded-full !flex items-center justify-center">
83+
<Crown className="w-2.5 h-2.5 text-amber-900" />
84+
</div>
85+
)}
86+
</div>
8087
</div>
81-
</div>
8288

83-
<ul
84-
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 dark:bg-black dark:text-gray-200 rounded-box w-52 border border-slate-200 dark:border-gray-800"
85-
onClick={handleCloseDropdown}
86-
tabIndex={0}
87-
>
88-
<li>
89-
<Link className="!no-underline" href="/profile" size="base" variant="nav">
90-
<User className="w-4 h-4 text-gray-700 dark:text-gray-300" />
91-
{t("commons.profile")}
92-
</Link>
93-
</li>
89+
<ul
90+
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 dark:bg-black dark:text-gray-200 rounded-box w-52 border border-slate-200 dark:border-gray-800"
91+
onClick={handleCloseDropdown}
92+
tabIndex={0}
93+
>
94+
<li>
95+
<Link className="!no-underline" href="/profile" size="base" variant="nav">
96+
<User className="w-4 h-4 text-gray-700 dark:text-gray-300" />
97+
{t("commons.profile")}
98+
</Link>
99+
</li>
94100

95-
{/* Subscription Menu Item */}
96-
<li>
97-
<Link
98-
className="!no-underline"
99-
href={isPremium ? "/api/premium/billing-portal" : "/premium"}
100-
size="base"
101-
variant="nav"
102-
{...(isPremium && {
103-
onClick: async (e) => {
104-
e.preventDefault();
105-
try {
106-
const response = await fetch("/api/premium/billing-portal", {
107-
method: "POST",
108-
headers: { "Content-Type": "application/json" },
109-
body: JSON.stringify({ returnUrl: window.location.origin + "/profile" }),
110-
});
111-
const data = await response.json();
112-
if (data.success && data.url) {
113-
window.location.href = data.url;
101+
{/* Subscription Menu Item */}
102+
<li>
103+
<Link
104+
className="!no-underline"
105+
href={isPremium ? "/api/premium/billing-portal" : "/premium"}
106+
size="base"
107+
variant="nav"
108+
{...(isPremium && {
109+
onClick: async (e) => {
110+
e.preventDefault();
111+
try {
112+
const response = await fetch("/api/premium/billing-portal", {
113+
method: "POST",
114+
headers: { "Content-Type": "application/json" },
115+
body: JSON.stringify({ returnUrl: window.location.origin + "/profile" }),
116+
});
117+
const data = await response.json();
118+
if (data.success && data.url) {
119+
window.location.href = data.url;
120+
}
121+
} catch (error) {
122+
console.error("Error opening billing portal:", error);
114123
}
115-
} catch (error) {
116-
console.error("Error opening billing portal:", error);
117-
}
118-
},
119-
})}
120-
>
121-
{isPremium ? (
122-
<>
123-
<Crown className="w-4 h-4 text-amber-500" />
124-
{t("commons.manage_subscription")}
125-
</>
126-
) : (
127-
<>
128-
<CreditCard className="w-4 h-4 text-blue-500" />
129-
{t("commons.become_premium")}
130-
</>
131-
)}
132-
</Link>
133-
</li>
124+
},
125+
})}
126+
>
127+
{isPremium ? (
128+
<>
129+
<Crown className="w-4 h-4 text-amber-500" />
130+
{t("commons.manage_subscription")}
131+
</>
132+
) : (
133+
<>
134+
<Sparkles className="w-4 h-4 text-purple-500" />
135+
{t("commons.remove_ads")}
136+
</>
137+
)}
138+
</Link>
139+
</li>
134140

135-
<hr className="my-1 border-slate-200 dark:border-gray-800" />
141+
<hr className="my-1 border-slate-200 dark:border-gray-800" />
136142

137-
{!session.data && !session.isPending ? (
138-
<>
143+
{!session.data && !session.isPending ? (
144+
<>
145+
<li>
146+
<Link className="!no-underline" href="/auth/signin" size="base" variant="nav">
147+
<LogIn className="w-4 h-4 text-gray-700 dark:text-gray-300" />
148+
{t("commons.login")}
149+
</Link>
150+
</li>
151+
<li>
152+
<Link className="!no-underline" href="/auth/signup" size="base" variant="nav">
153+
<UserPlus className="w-4 h-4 text-gray-700 dark:text-gray-300" />
154+
{t("commons.register")}
155+
</Link>
156+
</li>
157+
</>
158+
) : (
139159
<li>
140-
<Link className="!no-underline" href="/auth/signin" size="base" variant="nav">
141-
<LogIn className="w-4 h-4 text-gray-700 dark:text-gray-300" />
142-
{t("commons.login")}
143-
</Link>
160+
<button
161+
className="flex items-center gap-2 text-base text-gray-700 dark:text-gray-300 hover:bg-slate-200 dark:hover:bg-gray-800 rounded-lg px-3 py-2 transition-colors"
162+
onClick={handleSignOut}
163+
>
164+
<LogOut className="w-4 h-4" />
165+
{t("commons.logout")}
166+
</button>
144167
</li>
145-
<li>
146-
<Link className="!no-underline" href="/auth/signup" size="base" variant="nav">
147-
<UserPlus className="w-4 h-4 text-gray-700 dark:text-gray-300" />
148-
{t("commons.register")}
149-
</Link>
150-
</li>
151-
</>
152-
) : (
153-
<li>
154-
<button
155-
className="flex items-center gap-2 text-base text-gray-700 dark:text-gray-300 hover:bg-slate-200 dark:hover:bg-gray-800 rounded-lg px-3 py-2 transition-colors"
156-
onClick={handleSignOut}
157-
>
158-
<LogOut className="w-4 h-4" />
159-
{t("commons.logout")}
160-
</button>
161-
</li>
162-
)}
163-
</ul>
168+
)}
169+
</ul>
170+
</div>
164171
</div>
165172
</div>
166-
</div>
173+
</>
167174
);
168175
};

0 commit comments

Comments
 (0)