|
1 | | -import { useQuery } from '@apollo/client/react'; |
2 | | -import { ApolloErrorLike } from '@sb/webapp-api-client/api/apolloError.types'; |
3 | | -import { SchemaType } from '@sb/webapp-api-client'; |
4 | | -import { PageLayout } from '@sb/webapp-core/components/pageLayout'; |
5 | | -import { Paragraph } from '@sb/webapp-core/components/typography'; |
6 | | -import { Alert, AlertDescription, AlertTitle } from '@sb/webapp-core/components/ui/alert'; |
7 | | -import { Card, CardContent, CardHeader } from '@sb/webapp-core/components/ui/card'; |
8 | | -import { Skeleton } from '@sb/webapp-core/components/ui/skeleton'; |
9 | | -import { AlertCircle, ExternalLink, FileText, Info, RefreshCw } from 'lucide-react'; |
10 | | -import { FC } from 'react'; |
11 | | -import { Helmet } from 'react-helmet-async'; |
12 | | -import { FormattedMessage, useIntl } from 'react-intl'; |
13 | | -import ReactMarkdown from 'react-markdown'; |
| 1 | +import { ContentfulContentPage } from '../../components/contentfulContentPage'; |
| 2 | +import { privacyPolicyConfig } from '../../components/contentfulContentPage/privacyPolicy.config'; |
14 | 3 |
|
15 | | -import { configContentfulAppQuery } from '../../config/config.graphql'; |
16 | | - |
17 | | -const LoadingSkeleton = () => ( |
18 | | - <PageLayout> |
19 | | - <div className="mx-auto w-full max-w-4xl space-y-8"> |
20 | | - <div className="space-y-4"> |
21 | | - <div className="flex items-center gap-2"> |
22 | | - <Skeleton className="h-6 w-6 rounded" /> |
23 | | - <Skeleton className="h-10 w-64" /> |
24 | | - </div> |
25 | | - <Skeleton className="h-6 w-96" /> |
26 | | - </div> |
27 | | - <Card> |
28 | | - <CardHeader> |
29 | | - <Skeleton className="h-6 w-48" /> |
30 | | - </CardHeader> |
31 | | - <CardContent className="space-y-4"> |
32 | | - <Skeleton className="h-4 w-full" /> |
33 | | - <Skeleton className="h-4 w-full" /> |
34 | | - <Skeleton className="h-4 w-3/4" /> |
35 | | - <Skeleton className="h-4 w-full" /> |
36 | | - <Skeleton className="h-4 w-5/6" /> |
37 | | - <Skeleton className="h-4 w-full" /> |
38 | | - <Skeleton className="h-4 w-2/3" /> |
39 | | - </CardContent> |
40 | | - </Card> |
41 | | - </div> |
42 | | - </PageLayout> |
| 4 | +export const PrivacyPolicy = () => ( |
| 5 | + <ContentfulContentPage config={privacyPolicyConfig} /> |
43 | 6 | ); |
44 | | - |
45 | | -type NotConfiguredStateProps = { |
46 | | - onRetry?: () => void; |
47 | | - isRefetching?: boolean; |
48 | | -}; |
49 | | - |
50 | | -const NotConfiguredState: FC<NotConfiguredStateProps> = ({ onRetry, isRefetching }) => { |
51 | | - const envFilePath = 'packages/webapp/.env'; |
52 | | - const docsUrl = |
53 | | - 'https://docs.demo.saas.apptension.com/working-with-sb/contentful/configure-contentful-integration'; |
54 | | - |
55 | | - return ( |
56 | | - <PageLayout> |
57 | | - <div className="mx-auto w-full max-w-4xl space-y-8"> |
58 | | - <div className="space-y-4"> |
59 | | - <div className="flex items-center gap-2"> |
60 | | - <FileText className="h-6 w-6 text-primary" /> |
61 | | - <h1 className="text-3xl font-bold tracking-tight"> |
62 | | - <FormattedMessage defaultMessage="Privacy Policy" id="Privacy Policy / Title" /> |
63 | | - </h1> |
64 | | - </div> |
65 | | - <Paragraph className="text-lg text-muted-foreground"> |
66 | | - <FormattedMessage |
67 | | - defaultMessage="How we handle and protect your data" |
68 | | - id="Privacy Policy / Description" |
69 | | - /> |
70 | | - </Paragraph> |
71 | | - </div> |
72 | | - |
73 | | - <Card className="border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20"> |
74 | | - <CardContent className="py-8"> |
75 | | - <div className="flex flex-col items-center justify-center text-center"> |
76 | | - <div className="rounded-full bg-blue-100 dark:bg-blue-900/50 p-4 mb-4"> |
77 | | - <Info className="h-8 w-8 text-blue-600 dark:text-blue-400" /> |
78 | | - </div> |
79 | | - <h3 className="text-lg font-semibold mb-2"> |
80 | | - <FormattedMessage |
81 | | - defaultMessage="Contentful Integration Not Configured" |
82 | | - id="Privacy Policy / Not configured title" |
83 | | - /> |
84 | | - </h3> |
85 | | - <p className="text-sm text-muted-foreground mb-6 max-w-lg"> |
86 | | - <FormattedMessage |
87 | | - defaultMessage="This page displays content managed via Contentful CMS. To enable it, you need to configure the Contentful integration." |
88 | | - id="Privacy Policy / Not configured description" |
89 | | - /> |
90 | | - </p> |
91 | | - |
92 | | - <div className="w-full max-w-lg text-left space-y-4"> |
93 | | - <div className="space-y-2"> |
94 | | - <h4 className="text-sm font-medium"> |
95 | | - <FormattedMessage defaultMessage="Quick Setup" id="Privacy Policy / Quick setup title" /> |
96 | | - </h4> |
97 | | - <ol className="text-sm text-muted-foreground space-y-3 list-decimal list-inside"> |
98 | | - <li> |
99 | | - <FormattedMessage |
100 | | - defaultMessage="Create a Contentful account and space at {link}" |
101 | | - id="Privacy Policy / Setup step 1" |
102 | | - values={{ |
103 | | - link: ( |
104 | | - <a |
105 | | - href="https://www.contentful.com/" |
106 | | - target="_blank" |
107 | | - rel="noopener noreferrer" |
108 | | - className="text-primary hover:underline inline-flex items-center gap-1" |
109 | | - > |
110 | | - contentful.com |
111 | | - <ExternalLink className="h-3 w-3" /> |
112 | | - </a> |
113 | | - ), |
114 | | - }} |
115 | | - /> |
116 | | - </li> |
117 | | - <li> |
118 | | - <FormattedMessage |
119 | | - defaultMessage="Add these environment variables to {file}:" |
120 | | - id="Privacy Policy / Setup step 2" |
121 | | - values={{ |
122 | | - file: <code className="bg-muted px-1.5 py-0.5 rounded text-xs">{envFilePath}</code>, |
123 | | - }} |
124 | | - /> |
125 | | - <code className="block mt-2 ml-4 p-3 bg-muted rounded text-xs font-mono whitespace-pre"> |
126 | | - VITE_CONTENTFUL_SPACE=your_space_id{'\n'} |
127 | | - VITE_CONTENTFUL_TOKEN=your_access_token{'\n'} |
128 | | - VITE_CONTENTFUL_ENV=master |
129 | | - </code> |
130 | | - </li> |
131 | | - <li> |
132 | | - <FormattedMessage |
133 | | - defaultMessage="Create an AppConfig content type with a 'privacyPolicy' field (Long text, Markdown)" |
134 | | - id="Privacy Policy / Setup step 3" |
135 | | - /> |
136 | | - </li> |
137 | | - <li> |
138 | | - <FormattedMessage |
139 | | - defaultMessage="Restart the development server" |
140 | | - id="Privacy Policy / Setup step 4" |
141 | | - /> |
142 | | - </li> |
143 | | - </ol> |
144 | | - </div> |
145 | | - |
146 | | - <div className="flex flex-wrap items-center justify-center gap-4 pt-4"> |
147 | | - {onRetry && ( |
148 | | - <button |
149 | | - onClick={onRetry} |
150 | | - disabled={isRefetching} |
151 | | - className="inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline disabled:opacity-50" |
152 | | - > |
153 | | - <RefreshCw className={`h-4 w-4 ${isRefetching ? 'animate-spin' : ''}`} /> |
154 | | - <FormattedMessage defaultMessage="Retry" id="Privacy Policy / Retry button" /> |
155 | | - </button> |
156 | | - )} |
157 | | - <a |
158 | | - href={docsUrl} |
159 | | - target="_blank" |
160 | | - rel="noopener noreferrer" |
161 | | - className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline" |
162 | | - > |
163 | | - <FormattedMessage |
164 | | - defaultMessage="View Full Documentation" |
165 | | - id="Privacy Policy / Docs link" |
166 | | - /> |
167 | | - <ExternalLink className="h-3 w-3" /> |
168 | | - </a> |
169 | | - </div> |
170 | | - </div> |
171 | | - </div> |
172 | | - </CardContent> |
173 | | - </Card> |
174 | | - </div> |
175 | | - </PageLayout> |
176 | | - ); |
177 | | -}; |
178 | | - |
179 | | -type ErrorStateProps = { |
180 | | - error: Error | ApolloErrorLike; |
181 | | - onRetry: () => void; |
182 | | - isRefetching: boolean; |
183 | | -}; |
184 | | - |
185 | | -const ErrorState: FC<ErrorStateProps> = ({ error, onRetry, isRefetching }) => { |
186 | | - // Check if this is a configuration/network error (Contentful not set up) |
187 | | - // Only check network errors - don't check env vars here as that's a build-time concern |
188 | | - const apolloError = error as ApolloErrorLike; |
189 | | - const isNetworkError = |
190 | | - apolloError?.networkError || |
191 | | - error?.message?.includes('fetch') || |
192 | | - error?.message?.includes('network') || |
193 | | - error?.message?.includes('Failed to fetch'); |
194 | | - |
195 | | - if (isNetworkError) { |
196 | | - return <NotConfiguredState onRetry={onRetry} isRefetching={isRefetching} />; |
197 | | - } |
198 | | - |
199 | | - return ( |
200 | | - <PageLayout> |
201 | | - <div className="mx-auto w-full max-w-4xl space-y-8"> |
202 | | - <div className="space-y-4"> |
203 | | - <div className="flex items-center gap-2"> |
204 | | - <FileText className="h-6 w-6 text-primary" /> |
205 | | - <h1 className="text-3xl font-bold tracking-tight"> |
206 | | - <FormattedMessage defaultMessage="Privacy Policy" id="Privacy Policy / Title" /> |
207 | | - </h1> |
208 | | - </div> |
209 | | - <Paragraph className="text-lg text-muted-foreground"> |
210 | | - <FormattedMessage |
211 | | - defaultMessage="How we handle and protect your data" |
212 | | - id="Privacy Policy / Description" |
213 | | - /> |
214 | | - </Paragraph> |
215 | | - </div> |
216 | | - |
217 | | - <Alert variant="destructive" className="border-destructive/50 bg-destructive/10"> |
218 | | - <AlertCircle className="h-5 w-5" /> |
219 | | - <AlertTitle className="font-semibold"> |
220 | | - <FormattedMessage |
221 | | - defaultMessage="Unable to load privacy policy" |
222 | | - id="Privacy Policy / Error title" |
223 | | - /> |
224 | | - </AlertTitle> |
225 | | - <AlertDescription className="mt-2 space-y-3"> |
226 | | - <p> |
227 | | - <FormattedMessage |
228 | | - defaultMessage="There was an error loading this content from Contentful." |
229 | | - id="Privacy Policy / Error description" |
230 | | - /> |
231 | | - </p> |
232 | | - {error.message && ( |
233 | | - <p className="text-xs font-mono bg-background/50 p-2 rounded border">{error.message}</p> |
234 | | - )} |
235 | | - <div className="flex gap-4"> |
236 | | - <button |
237 | | - onClick={onRetry} |
238 | | - disabled={isRefetching} |
239 | | - className="inline-flex items-center gap-2 text-sm font-medium hover:underline disabled:opacity-50" |
240 | | - > |
241 | | - <RefreshCw className={`h-4 w-4 ${isRefetching ? 'animate-spin' : ''}`} /> |
242 | | - <FormattedMessage defaultMessage="Try again" id="Privacy Policy / Retry button" /> |
243 | | - </button> |
244 | | - <a |
245 | | - href="https://www.contentful.com/developers/docs/" |
246 | | - target="_blank" |
247 | | - rel="noopener noreferrer" |
248 | | - className="inline-flex items-center gap-1 text-sm hover:underline" |
249 | | - > |
250 | | - <FormattedMessage defaultMessage="Contentful Documentation" id="Privacy Policy / Docs link" /> |
251 | | - <ExternalLink className="h-3 w-3" /> |
252 | | - </a> |
253 | | - </div> |
254 | | - </AlertDescription> |
255 | | - </Alert> |
256 | | - </div> |
257 | | - </PageLayout> |
258 | | - ); |
259 | | -}; |
260 | | - |
261 | | -type ContentStateProps = { |
262 | | - markdown: string; |
263 | | -}; |
264 | | - |
265 | | -const ContentState: FC<ContentStateProps> = ({ markdown }) => { |
266 | | - const intl = useIntl(); |
267 | | - |
268 | | - return ( |
269 | | - <PageLayout> |
270 | | - <Helmet |
271 | | - title={intl.formatMessage({ |
272 | | - defaultMessage: 'Privacy Policy', |
273 | | - id: 'Privacy Policy / Page title', |
274 | | - })} |
275 | | - /> |
276 | | - <div className="mx-auto w-full max-w-4xl space-y-8"> |
277 | | - <div className="space-y-4"> |
278 | | - <div className="flex items-center gap-2"> |
279 | | - <FileText className="h-6 w-6 text-primary" /> |
280 | | - <h1 className="text-3xl font-bold tracking-tight"> |
281 | | - <FormattedMessage defaultMessage="Privacy Policy" id="Privacy Policy / Title" /> |
282 | | - </h1> |
283 | | - </div> |
284 | | - <Paragraph className="text-lg text-muted-foreground"> |
285 | | - <FormattedMessage |
286 | | - defaultMessage="How we handle and protect your data" |
287 | | - id="Privacy Policy / Description" |
288 | | - /> |
289 | | - </Paragraph> |
290 | | - </div> |
291 | | - |
292 | | - <Card> |
293 | | - <CardContent className="py-6"> |
294 | | - <div className="prose prose-sm dark:prose-invert max-w-none prose-headings:font-semibold prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-p:text-muted-foreground prose-li:text-muted-foreground prose-a:text-primary"> |
295 | | - <ReactMarkdown>{markdown}</ReactMarkdown> |
296 | | - </div> |
297 | | - </CardContent> |
298 | | - </Card> |
299 | | - </div> |
300 | | - </PageLayout> |
301 | | - ); |
302 | | -}; |
303 | | - |
304 | | -const EmptyContentState: FC = () => { |
305 | | - const intl = useIntl(); |
306 | | - |
307 | | - return ( |
308 | | - <PageLayout> |
309 | | - <Helmet |
310 | | - title={intl.formatMessage({ |
311 | | - defaultMessage: 'Privacy Policy', |
312 | | - id: 'Privacy Policy / Page title', |
313 | | - })} |
314 | | - /> |
315 | | - <div className="mx-auto w-full max-w-4xl space-y-8"> |
316 | | - <div className="space-y-4"> |
317 | | - <div className="flex items-center gap-2"> |
318 | | - <FileText className="h-6 w-6 text-primary" /> |
319 | | - <h1 className="text-3xl font-bold tracking-tight"> |
320 | | - <FormattedMessage defaultMessage="Privacy Policy" id="Privacy Policy / Title" /> |
321 | | - </h1> |
322 | | - </div> |
323 | | - <Paragraph className="text-lg text-muted-foreground"> |
324 | | - <FormattedMessage |
325 | | - defaultMessage="How we handle and protect your data" |
326 | | - id="Privacy Policy / Description" |
327 | | - /> |
328 | | - </Paragraph> |
329 | | - </div> |
330 | | - |
331 | | - <Card className="border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-950/20"> |
332 | | - <CardContent className="py-8"> |
333 | | - <div className="flex flex-col items-center justify-center text-center"> |
334 | | - <div className="rounded-full bg-amber-100 dark:bg-amber-900/50 p-4 mb-4"> |
335 | | - <FileText className="h-8 w-8 text-amber-600 dark:text-amber-400" /> |
336 | | - </div> |
337 | | - <h3 className="text-lg font-semibold mb-2"> |
338 | | - <FormattedMessage defaultMessage="No Content Available" id="Privacy Policy / Empty title" /> |
339 | | - </h3> |
340 | | - <p className="text-sm text-muted-foreground max-w-md mb-4"> |
341 | | - <FormattedMessage |
342 | | - defaultMessage="The privacy policy content hasn't been added yet. Please add content to the 'privacyPolicy' field in your Contentful AppConfig entry." |
343 | | - id="Privacy Policy / Empty description" |
344 | | - /> |
345 | | - </p> |
346 | | - <a |
347 | | - href="https://app.contentful.com/" |
348 | | - target="_blank" |
349 | | - rel="noopener noreferrer" |
350 | | - className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline" |
351 | | - > |
352 | | - <FormattedMessage defaultMessage="Open Contentful" id="Privacy Policy / Open Contentful" /> |
353 | | - <ExternalLink className="h-3 w-3" /> |
354 | | - </a> |
355 | | - </div> |
356 | | - </CardContent> |
357 | | - </Card> |
358 | | - </div> |
359 | | - </PageLayout> |
360 | | - ); |
361 | | -}; |
362 | | - |
363 | | -export const PrivacyPolicy = () => { |
364 | | - const { data, loading, error, refetch, networkStatus } = useQuery(configContentfulAppQuery, { |
365 | | - context: { schemaType: SchemaType.Contentful }, |
366 | | - notifyOnNetworkStatusChange: true, |
367 | | - errorPolicy: 'all', |
368 | | - }); |
369 | | - |
370 | | - const isRefetching = networkStatus === 4; // NetworkStatus.refetch |
371 | | - const isLoading = loading && !isRefetching && !data && !error; |
372 | | - |
373 | | - if (isLoading) { |
374 | | - return <LoadingSkeleton />; |
375 | | - } |
376 | | - |
377 | | - if (error) { |
378 | | - return <ErrorState error={error} onRetry={() => refetch()} isRefetching={isRefetching} />; |
379 | | - } |
380 | | - |
381 | | - const markdown = data?.appConfigCollection?.items?.[0]?.privacyPolicy; |
382 | | - |
383 | | - if (!markdown) { |
384 | | - return <EmptyContentState />; |
385 | | - } |
386 | | - |
387 | | - return <ContentState markdown={markdown} />; |
388 | | -}; |
0 commit comments