Skip to content

Commit c0c7258

Browse files
ChengaDevclaude
andcommitted
Add privacy policy, cookie consent banner, and contact section
- Privacy Policy page at /privacy-policy with Google Analytics and AdSense disclosures - Google Consent Mode v2: default-denied in index.html, updated on user choice - Cookie consent banner (slide-up) with Essential only / Accept all options - About page: new "Get in Touch" section with LinkedIn, GitHub, and personal site links - Footer: added FAQ and Privacy Policy to Quick Links - sitemap.xml and llms.txt updated to include /privacy-policy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9fa3115 commit c0c7258

9 files changed

Lines changed: 424 additions & 0 deletions

File tree

public/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@
2828
<meta property="twitter:image" content="%PUBLIC_URL%/og-image.png" />
2929
<meta property="twitter:image:alt" content="ShotClock Pro — free online basketball shot clock" />
3030

31+
<!-- Google Consent Mode v2 — default denied until user accepts the cookie banner -->
32+
<script>
33+
window.dataLayer = window.dataLayer || [];
34+
function gtag(){dataLayer.push(arguments);}
35+
gtag('consent', 'default', {
36+
'analytics_storage': 'denied',
37+
'ad_storage': 'denied',
38+
'ad_user_data': 'denied',
39+
'ad_personalization': 'denied',
40+
'wait_for_update': 500
41+
});
42+
</script>
43+
3144
<!-- Google tag (gtag.js) -->
3245
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PB7M6KHZ9Z"></script>
3346
<script>

public/llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [FIBA Resources](https://www.24shotclock.com/fiba-resources): Links to official FIBA basketball rules, shot clock guidelines, referee training materials, and competition manuals.
1010
- [FAQ](https://www.24shotclock.com/faq): Answers to common questions about shot clock rules, the 14-second reset, blind-clock drills, and how to use the app.
1111
- [About](https://www.24shotclock.com/about): Background on ShotClock Pro — its purpose, target users, and training philosophy.
12+
- [Privacy Policy](https://www.24shotclock.com/privacy-policy): Information about data collection, cookies, Google Analytics, and Google AdSense usage.
1213

1314
## Languages
1415

public/sitemap.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.24shotclock.com/faq"/>
6161
</url>
6262

63+
<!-- Privacy Policy (English only, no lang variants) -->
64+
<url>
65+
<loc>https://www.24shotclock.com/privacy-policy</loc>
66+
<lastmod>2026-04-07</lastmod>
67+
<changefreq>yearly</changefreq>
68+
<priority>0.3</priority>
69+
</url>
70+
6371
<!-- Italian -->
6472
<url>
6573
<loc>https://www.24shotclock.com/it</loc>

src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import LanguageProvider, { useLocalization } from './contexts/Language/LanguageP
55
import Navigation from './components/Navigation'
66
import ShareButtons from './components/ShareButtons'
77
import Footer from './components/Footer'
8+
import CookieBanner from './components/CookieBanner'
89
import { lightTheme, darkTheme } from './themes/themes'
910
import { createGlobalStyle } from 'styled-components'
1011
import './assets/fonts/dseg14/dseg14.css'
@@ -16,6 +17,7 @@ const AboutUs = lazy(() => import('./components/AboutUs'))
1617
const Instructions = lazy(() => import('./components/Instructions'))
1718
const FIBAResources = lazy(() => import('./components/FIBAResources'))
1819
const FAQ = lazy(() => import('./components/FAQ'))
20+
const PrivacyPolicy = lazy(() => import('./components/PrivacyPolicy'))
1921
const NotFound = lazy(() => import('./components/NotFound'))
2022

2123
const NON_ENGLISH_LANGS = ['it', 'es', 'fr']
@@ -110,6 +112,11 @@ const pageRoutes = (
110112
<FAQ />
111113
</PageContent>
112114
} />
115+
<Route path="privacy-policy" element={
116+
<PageContent title="Privacy Policy - ShotClock Pro">
117+
<PrivacyPolicy />
118+
</PageContent>
119+
} />
113120
</>
114121
)
115122

@@ -144,6 +151,7 @@ const App = () => {
144151
</MainContent>
145152
<Footer />
146153
</PageWrapper>
154+
<CookieBanner />
147155
</AppContainer>
148156
</LanguageProvider>
149157
</Router>

src/components/AboutUs.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ const AboutUs = () => {
4646
</FeatureCard>
4747
))}
4848
</Features>
49+
50+
<ContactSection>
51+
<ContactTitle>Get in Touch</ContactTitle>
52+
<ContactText>
53+
Questions, feedback, or just want to say hi? Reach out through any of the channels below.
54+
</ContactText>
55+
<ContactLinks>
56+
<ContactLink href="https://www.linkedin.com/in/chengazit/" target="_blank" rel="noreferrer noopener">
57+
<i className="fab fa-linkedin" />
58+
LinkedIn
59+
</ContactLink>
60+
<ContactLink href="https://github.com/ChengaDev/" target="_blank" rel="noreferrer noopener">
61+
<i className="fab fa-github" />
62+
GitHub
63+
</ContactLink>
64+
<ContactLink href="https://chengazit.com/" target="_blank" rel="noreferrer noopener">
65+
<i className="fas fa-globe" />
66+
chengazit.com
67+
</ContactLink>
68+
</ContactLinks>
69+
</ContactSection>
4970
</Container>
5071
)
5172
}
@@ -155,4 +176,48 @@ const FeatureText = styled.p`
155176
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
156177
`
157178

179+
const ContactSection = styled.div`
180+
margin-top: 4rem;
181+
text-align: center;
182+
`
183+
184+
const ContactTitle = styled.h2`
185+
font-size: 1.8rem;
186+
color: #ffd700;
187+
margin-bottom: 1rem;
188+
font-weight: 700;
189+
`
190+
191+
const ContactText = styled.p`
192+
color: rgba(255, 255, 255, 0.75);
193+
font-size: 1rem;
194+
margin-bottom: 2rem;
195+
`
196+
197+
const ContactLinks = styled.div`
198+
display: flex;
199+
justify-content: center;
200+
flex-wrap: wrap;
201+
gap: 1rem;
202+
`
203+
204+
const ContactLink = styled.a`
205+
display: inline-flex;
206+
align-items: center;
207+
gap: 0.5rem;
208+
padding: 0.65rem 1.5rem;
209+
border-radius: 8px;
210+
border: 1px solid rgba(255, 215, 0, 0.35);
211+
color: #ffd700;
212+
font-size: 0.95rem;
213+
font-weight: 600;
214+
text-decoration: none;
215+
transition: all 0.2s ease;
216+
217+
&:hover {
218+
background: rgba(255, 215, 0, 0.1);
219+
border-color: #ffd700;
220+
}
221+
`
222+
158223
export default AboutUs

src/components/CookieBanner.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useEffect, useState } from 'react'
2+
import styled, { keyframes } from 'styled-components'
3+
import { Link } from 'react-router-dom'
4+
5+
type ConsentValue = 'granted' | 'denied' | null
6+
7+
const STORAGE_KEY = 'cookie_consent'
8+
9+
function updateGoogleConsent(value: 'granted' | 'denied') {
10+
if (typeof window.gtag === 'function') {
11+
window.gtag('consent', 'update', {
12+
analytics_storage: value,
13+
ad_storage: value,
14+
ad_user_data: value,
15+
ad_personalization: value,
16+
})
17+
}
18+
}
19+
20+
const CookieBanner = () => {
21+
const [consent, setConsent] = useState<ConsentValue>(null)
22+
const [visible, setVisible] = useState(false)
23+
24+
useEffect(() => {
25+
const stored = localStorage.getItem(STORAGE_KEY) as ConsentValue
26+
if (stored === 'granted' || stored === 'denied') {
27+
// Re-apply saved choice on every page load so Consent Mode is always in sync
28+
updateGoogleConsent(stored)
29+
setConsent(stored)
30+
} else {
31+
// No prior choice — show banner
32+
setVisible(true)
33+
}
34+
}, [])
35+
36+
const handleAccept = () => {
37+
localStorage.setItem(STORAGE_KEY, 'granted')
38+
updateGoogleConsent('granted')
39+
setConsent('granted')
40+
setVisible(false)
41+
}
42+
43+
const handleDecline = () => {
44+
localStorage.setItem(STORAGE_KEY, 'denied')
45+
updateGoogleConsent('denied')
46+
setConsent('denied')
47+
setVisible(false)
48+
}
49+
50+
if (!visible) return null
51+
52+
return (
53+
<Banner role="dialog" aria-label="Cookie consent">
54+
<Text>
55+
We use cookies for analytics and advertising. See our{' '}
56+
<PolicyLink to="/privacy-policy">Privacy Policy</PolicyLink>.
57+
</Text>
58+
<Buttons>
59+
<DeclineButton onClick={handleDecline}>Essential only</DeclineButton>
60+
<AcceptButton onClick={handleAccept}>Accept all</AcceptButton>
61+
</Buttons>
62+
</Banner>
63+
)
64+
}
65+
66+
const slideUp = keyframes`
67+
from { transform: translateY(100%); opacity: 0; }
68+
to { transform: translateY(0); opacity: 1; }
69+
`
70+
71+
const Banner = styled.div`
72+
position: fixed;
73+
bottom: 0;
74+
left: 0;
75+
right: 0;
76+
z-index: 1000;
77+
background: rgba(15, 15, 15, 0.97);
78+
border-top: 1px solid rgba(255, 215, 0, 0.25);
79+
backdrop-filter: blur(10px);
80+
padding: 1rem 2rem;
81+
display: flex;
82+
align-items: center;
83+
justify-content: space-between;
84+
gap: 1.5rem;
85+
animation: ${slideUp} 0.35s ease-out;
86+
87+
@media (max-width: 600px) {
88+
flex-direction: column;
89+
align-items: flex-start;
90+
padding: 1rem 1.25rem;
91+
}
92+
`
93+
94+
const Text = styled.p`
95+
margin: 0;
96+
color: rgba(255, 255, 255, 0.75);
97+
font-size: 0.9rem;
98+
line-height: 1.5;
99+
flex: 1;
100+
`
101+
102+
const PolicyLink = styled(Link)`
103+
color: #ffd700;
104+
text-decoration: underline;
105+
&:hover {
106+
color: #ffe566;
107+
}
108+
`
109+
110+
const Buttons = styled.div`
111+
display: flex;
112+
gap: 0.75rem;
113+
flex-shrink: 0;
114+
115+
@media (max-width: 600px) {
116+
width: 100%;
117+
justify-content: flex-end;
118+
}
119+
`
120+
121+
const BaseButton = styled.button`
122+
padding: 0.5rem 1.25rem;
123+
border-radius: 6px;
124+
font-size: 0.875rem;
125+
font-weight: 600;
126+
cursor: pointer;
127+
transition: all 0.2s ease;
128+
border: none;
129+
`
130+
131+
const DeclineButton = styled(BaseButton)`
132+
background: transparent;
133+
border: 1px solid rgba(255, 255, 255, 0.25);
134+
color: rgba(255, 255, 255, 0.65);
135+
&:hover {
136+
border-color: rgba(255, 255, 255, 0.5);
137+
color: rgba(255, 255, 255, 0.9);
138+
}
139+
`
140+
141+
const AcceptButton = styled(BaseButton)`
142+
background: #ffd700;
143+
color: #111;
144+
&:hover {
145+
background: #ffe566;
146+
}
147+
`
148+
149+
export default CookieBanner

src/components/Footer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const Footer = () => {
2121
<FooterLinks>
2222
<FooterLink href="/instructions">{locals.instructions}</FooterLink>
2323
<FooterLink href="/fiba-resources">{locals.fibaResources}</FooterLink>
24+
<FooterLink href="/faq">{locals.faq}</FooterLink>
2425
<FooterLink href="/about">{locals.about}</FooterLink>
26+
<FooterLink href="/privacy-policy">Privacy Policy</FooterLink>
2527
</FooterLinks>
2628
</FooterSection>
2729

0 commit comments

Comments
 (0)