1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
import { useEffect, useState } from 'react'
import { fetchDrupalResource } from './lib/drupalClient'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
type DrupalArticle = {
id: string
attributes?: {
title?: string
status?: boolean
created?: string
}
}
type DrupalCollectionResponse<T> = {
data: T[]
}
type DrupalJsonApiEntryPoint = {
links: Record<string, { href: string }>
}
function App() {
const [articles, setArticles] = useState<DrupalArticle[]>([])
const [resourcePath, setResourcePath] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const loadArticles = async () => {
try {
setIsLoading(true)
setError(null)
const entryPoint = await fetchDrupalResource<DrupalJsonApiEntryPoint>('')
const nodeResources = Object.keys(entryPoint.links).filter((key) => key.startsWith('node--'))
if (nodeResources.length === 0) {
throw new Error('No node resources found in JSON:API. Create a content type (for example Article) and ensure JSON:API is enabled.')
}
const selectedNodeResource = nodeResources.includes('node--article') ? 'node--article' : nodeResources[0]
const selectedPath = `/${selectedNodeResource.replace('--', '/')}`
setResourcePath(selectedPath)
const response = await fetchDrupalResource<DrupalCollectionResponse<DrupalArticle>>(selectedPath)
setArticles(response.data)
} catch (loadError) {
const message = loadError instanceof Error ? loadError.message : 'Failed to load articles from Drupal.'
setError(message)
} finally {
setIsLoading(false)
}
}
void loadArticles()
}, [])
const articleCount = articles.length
const formatDate = (value?: string) => {
if (!value) {
return 'Unknown date'
}
return new Date(value).toLocaleString()
}
return (
<main className="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-4 py-10 md:px-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Iwan Ingman's Portfolio</CardTitle>
<CardDescription>This is my portfolio site built with React and Drupal CMS.</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap items-center gap-3">
<Badge variant={isLoading ? 'secondary' : error ? 'destructive' : 'default'}>
{isLoading ? 'Loading' : error ? 'Error' : 'Connected'}
</Badge>
<Badge variant="outline">Articles: {articleCount}</Badge>
{resourcePath && <Badge variant="outline">Resource: {resourcePath}</Badge>}
</CardContent>
<CardFooter className="justify-between gap-3">
<span className="text-muted-foreground">Last updated: {new Date().toLocaleString()}</span>
<Button type="button" onClick={() => window.location.reload()}>
Refresh
</Button>
</CardFooter>
</Card>
{error && (
<Alert variant="destructive">
<AlertTitle>Could not fetch articles</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{!isLoading && !error && articleCount === 0 && (
<Alert>
<AlertTitle>No articles found</AlertTitle>
<AlertDescription>Connected to Drupal successfully, but this resource currently has no content.</AlertDescription>
</Alert>
)}
{!isLoading && !error && articleCount > 0 && (
<section className="grid gap-4 md:grid-cols-2">
{articles.map((article) => (
<Card key={article.id} size="sm">
<CardHeader className="gap-2">
<CardTitle>{article.attributes?.title ?? '(Untitled)'}</CardTitle>
<CardDescription>{article.id}</CardDescription>
</CardHeader>
<Separator />
<CardContent className="space-y-2 pt-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Status</span>
<Badge variant={article.attributes?.status ? 'default' : 'secondary'}>
{article.attributes?.status ? 'Published' : 'Unpublished'}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Created</span>
<span>{formatDate(article.attributes?.created)}</span>
</div>
</CardContent>
</Card>
))}
</section>
)}
</main>
)
}
export default App
|