Skip to content

Commit c3f83db

Browse files
committed
chore: Refactor contentful changes
1 parent 02461e2 commit c3f83db

File tree

4 files changed

+26
-892
lines changed

4 files changed

+26
-892
lines changed
Lines changed: 4 additions & 386 deletions
Original file line numberDiff line numberDiff line change
@@ -1,388 +1,6 @@
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';
143

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} />
436
);
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

Comments
 (0)