Skip to content

Commit b9db776

Browse files
feat: add Google AdSense component and integrate it into the layout; refactor Appwrite client initialization to use environment variables from a new lib/appwrite/env module
1 parent 485ced7 commit b9db776

12 files changed

Lines changed: 130 additions & 32 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ NEXT_PUBLIC_APPWRITE_COLLECTION_ID_TRIALS=
1010
NEXT_PUBLIC_APPWRITE_DATABASE_ID=
1111
NEXT_PUBLIC_APPWRITE_ENDPOINT=
1212
NEXT_PUBLIC_APPWRITE_PROJECT_ID=
13+
NEXT_PUBLIC_ADSENSE_CLIENT_ID=
1314
NEXT_PUBLIC_GA_TRACKING_ID=
1415
NEXT_PUBLIC_STRIPE_PRICE_ADS_FREE=price_...
1516
NEXT_PUBLIC_STRIPE_PRICE_BYOK=price_...

app/api/explanations/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
22
import { Client, Databases, Query } from "node-appwrite";
33
import { ExplanationService } from "../../../lib/explanation-service";
44
import crypto from "crypto";
5+
import {
6+
appwriteApiKey,
7+
appwriteEndpoint,
8+
appwriteProjectId,
9+
} from "@practice-tests-exams-platform/lib/appwrite/env";
510

611
// Decrypt API key (same logic as in api-keys route)
712
function decryptApiKey(encryptedKey: string): string {
@@ -31,9 +36,9 @@ function decryptApiKey(encryptedKey: string): string {
3136
// Initialize Appwrite client
3237
function getAppwriteClient() {
3338
const client = new Client()
34-
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || "")
35-
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || "")
36-
.setKey(process.env.NEXT_PUBLIC_APPWRITE_API_KEY || "");
39+
.setEndpoint(appwriteEndpoint)
40+
.setProject(appwriteProjectId)
41+
.setKey(appwriteApiKey);
3742

3843
return new Databases(client);
3944
}

app/api/profile/api-keys/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { Client, Databases, Query } from "node-appwrite";
33
import crypto from "crypto";
4+
import {
5+
appwriteApiKey,
6+
appwriteEndpoint,
7+
appwriteProjectId,
8+
} from "@practice-tests-exams-platform/lib/appwrite/env";
49

510
// Simple encryption for API keys (uses environment variable as key)
611
function encryptApiKey(apiKey: string): string {
@@ -52,9 +57,9 @@ function decryptApiKey(encryptedKey: string): string {
5257
// Initialize Appwrite client
5358
function getAppwriteClient() {
5459
const client = new Client()
55-
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || "")
56-
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || "")
57-
.setKey(process.env.NEXT_PUBLIC_APPWRITE_API_KEY || "");
60+
.setEndpoint(appwriteEndpoint)
61+
.setProject(appwriteProjectId)
62+
.setKey(appwriteApiKey);
5863

5964
return new Databases(client);
6065
}

app/api/profile/preferences/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { Client, Databases, Query } from "node-appwrite";
3+
import {
4+
appwriteApiKey,
5+
appwriteEndpoint,
6+
appwriteProjectId,
7+
} from "@practice-tests-exams-platform/lib/appwrite/env";
38

49
// Initialize Appwrite client
510
function getAppwriteClient() {
611
const client = new Client()
7-
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || "")
8-
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || "")
9-
.setKey(process.env.NEXT_PUBLIC_APPWRITE_API_KEY || "");
12+
.setEndpoint(appwriteEndpoint)
13+
.setProject(appwriteProjectId)
14+
.setKey(appwriteApiKey);
1015

1116
return new Databases(client);
1217
}

app/api/profile/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { Client, Databases } from "node-appwrite";
33
import Stripe from "stripe";
4+
import {
5+
appwriteApiKey,
6+
appwriteEndpoint,
7+
appwriteProjectId,
8+
} from "@practice-tests-exams-platform/lib/appwrite/env";
49

510
// Initialize Appwrite client
611
function getAppwriteClient() {
712
const client = new Client()
8-
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || "")
9-
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || "")
10-
.setKey(process.env.NEXT_PUBLIC_APPWRITE_API_KEY || "");
13+
.setEndpoint(appwriteEndpoint)
14+
.setProject(appwriteProjectId)
15+
.setKey(appwriteApiKey);
1116

1217
return new Databases(client);
1318
}

app/api/stripe/webhook/route.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import Stripe from "stripe";
33
import { Client, Databases } from "node-appwrite";
4+
import {
5+
appwriteApiKey,
6+
appwriteEndpoint,
7+
appwriteProjectId,
8+
} from "@practice-tests-exams-platform/lib/appwrite/env";
49

510
// Force dynamic rendering for webhook endpoint
611
export const dynamic = "force-dynamic";
@@ -13,12 +18,12 @@ const PRICE_ID_TO_SUBSCRIPTION: Record<string, string> = {
1318
[(process.env.NEXT_PUBLIC_STRIPE_PRICE_DITECTREV || "").trim()]: "ditectrev",
1419
};
1520

16-
// Initialize Appwrite client (trim env vars to avoid newline issues)
21+
// Initialize Appwrite client (values trimmed in lib/appwrite/env.ts)
1722
function getAppwriteClient() {
1823
const client = new Client()
19-
.setEndpoint((process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || "").trim())
20-
.setProject((process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || "").trim())
21-
.setKey((process.env.NEXT_PUBLIC_APPWRITE_API_KEY || "").trim());
24+
.setEndpoint(appwriteEndpoint)
25+
.setProject(appwriteProjectId)
26+
.setKey(appwriteApiKey);
2227

2328
return new Databases(client);
2429
}

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Footer from "@practice-tests-exams-platform/components/Footer";
66
import ApolloProvider from "@practice-tests-exams-platform/components/ApolloProvider";
77
import Cookie from "@practice-tests-exams-platform/components/Cookie";
88
import GoogleAnalytics from "@practice-tests-exams-platform/components/GoogleAnalytics";
9+
import GoogleAdSense from "@practice-tests-exams-platform/components/GoogleAdSense";
910
import { AuthProvider } from "@practice-tests-exams-platform/contexts/AuthContext";
1011
import { ThemeProvider } from "@practice-tests-exams-platform/contexts/ThemeContext";
1112
import { TrialWarning } from "@practice-tests-exams-platform/components/TrialWarning";
@@ -135,6 +136,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
135136
<Footer />
136137
<Cookie />
137138
<GoogleAnalytics />
139+
<GoogleAdSense />
138140
<TrialWarning />
139141
</main>
140142
</AuthProvider>

components/AuthModal.tsx

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

33
import React, { useState, useEffect } from "react";
4+
import { createPortal } from "react-dom";
45
import { useAuth } from "../contexts/AuthContext";
56
import { Button } from "./Button";
67

@@ -34,6 +35,20 @@ export const AuthModal: React.FC<AuthModalProps> = ({
3435
type: string;
3536
value: string;
3637
} | null>(null);
38+
const [mounted, setMounted] = useState(false);
39+
40+
useEffect(() => {
41+
setMounted(true);
42+
}, []);
43+
44+
useEffect(() => {
45+
if (!isOpen) return;
46+
const previousOverflow = document.body.style.overflow;
47+
document.body.style.overflow = "hidden";
48+
return () => {
49+
document.body.style.overflow = previousOverflow;
50+
};
51+
}, [isOpen]);
3752

3853
// Load last used method from localStorage
3954
useEffect(() => {
@@ -67,7 +82,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
6782
}
6883
}, [isAuthenticated, isOpen, onClose]);
6984

70-
if (!isOpen) return null;
85+
if (!isOpen || !mounted) return null;
7186

7287
const handleEmailSignIn = async (e: React.FormEvent) => {
7388
e.preventDefault();
@@ -155,13 +170,30 @@ export const AuthModal: React.FC<AuthModalProps> = ({
155170
}
156171
};
157172

158-
return (
159-
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
160-
<div className="bg-slate-800 rounded-lg p-6 w-full max-w-md">
173+
return createPortal(
174+
<div
175+
className="fixed inset-0 z-[100] flex min-h-[100dvh] items-center justify-center overflow-y-auto bg-black/50 px-4 py-6 sm:py-10"
176+
style={{
177+
paddingTop: "max(1.5rem, env(safe-area-inset-top))",
178+
paddingBottom: "max(1.5rem, env(safe-area-inset-bottom))",
179+
}}
180+
role="presentation"
181+
onClick={onClose}
182+
>
183+
<div
184+
className="relative w-full max-w-md shrink-0 rounded-lg bg-slate-800 p-6 shadow-2xl"
185+
role="dialog"
186+
aria-modal="true"
187+
aria-labelledby="auth-modal-title"
188+
onClick={(e) => e.stopPropagation()}
189+
>
161190
<div className="text-center mb-6">
162191
{trialExpired ? (
163192
<>
164-
<h2 className="text-2xl font-bold text-white mb-2">
193+
<h2
194+
id="auth-modal-title"
195+
className="text-2xl font-bold text-white mb-2"
196+
>
165197
Trial Expired
166198
</h2>
167199
<p className="text-slate-300">
@@ -171,7 +203,12 @@ export const AuthModal: React.FC<AuthModalProps> = ({
171203
</>
172204
) : (
173205
<>
174-
<h2 className="text-2xl font-bold text-white mb-2">Sign In</h2>
206+
<h2
207+
id="auth-modal-title"
208+
className="text-2xl font-bold text-white mb-2"
209+
>
210+
Sign In
211+
</h2>
175212
<p className="text-slate-300">
176213
Choose your preferred sign-in method to continue.
177214
</p>
@@ -325,8 +362,8 @@ export const AuthModal: React.FC<AuthModalProps> = ({
325362
{isVerifying
326363
? "Verifying..."
327364
: isRedirecting
328-
? "Redirecting..."
329-
: "Verify Code"}
365+
? "Redirecting..."
366+
: "Verify Code"}
330367
</Button>
331368
</form>
332369

@@ -437,6 +474,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
437474
</button>
438475
</div>
439476
</div>
440-
</div>
477+
</div>,
478+
document.body,
441479
);
442480
};

components/GoogleAdSense.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Script from "next/script";
2+
3+
/**
4+
* Loads the AdSense bootstrap script. Set NEXT_PUBLIC_ADSENSE_CLIENT_ID (e.g. ca-pub-…)
5+
* in the environment. Place ad units in the UI or enable Auto ads in the AdSense console.
6+
*/
7+
export default function GoogleAdSense() {
8+
const clientId = process.env.NEXT_PUBLIC_ADSENSE_CLIENT_ID?.trim();
9+
if (!clientId) return null;
10+
11+
return (
12+
<Script
13+
async
14+
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${clientId}`}
15+
crossOrigin="anonymous"
16+
strategy="afterInteractive"
17+
/>
18+
);
19+
}

lib/appwrite/config.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { Client, Account, Databases } from "appwrite";
2+
import { appwriteEndpoint, appwriteProjectId } from "./env";
23

34
// Only initialize Appwrite client if we have the required environment variables
45
// This prevents build-time errors during static generation
56
const isClientSide = typeof window !== "undefined";
67
const hasRequiredEnvVars =
7-
process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT &&
8-
process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
8+
appwriteEndpoint.length > 0 && appwriteProjectId.length > 0;
99

1010
const client =
1111
isClientSide && hasRequiredEnvVars
12-
? new Client()
13-
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
14-
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
12+
? new Client().setEndpoint(appwriteEndpoint).setProject(appwriteProjectId)
1513
: null;
1614

1715
export const account = client ? new Account(client) : null;

0 commit comments

Comments
 (0)