summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.example4
-rw-r--r--README.md31
-rw-r--r--docker-compose.local.yml37
-rw-r--r--src/App.tsx107
-rw-r--r--vite.config.ts9
5 files changed, 161 insertions, 27 deletions
diff --git a/.env.example b/.env.example
index df591d0..fece7bb 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,3 @@
-VITE_DRUPAL_BASE_URL=https://your-drupal-domain.tld
-VITE_DRUPAL_API_PREFIX=/jsonapi
+VITE_DRUPAL_BASE_URL=http://localhost:5173
+VITE_DRUPAL_API_PREFIX=/drupal-api/jsonapi
VITE_DRUPAL_AUTH_TOKEN=
diff --git a/README.md b/README.md
index 5e16435..a059811 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,11 @@ Available variables:
- `VITE_DRUPAL_API_PREFIX` (optional) – defaults to `/jsonapi`
- `VITE_DRUPAL_AUTH_TOKEN` (optional) – bearer token used by the HTTP client
+For local development with the included Drupal stack (CORS-safe via Vite proxy), use:
+
+- `VITE_DRUPAL_BASE_URL=http://localhost:5173`
+- `VITE_DRUPAL_API_PREFIX=/drupal-api/jsonapi`
+
## Client utilities
- Typed env config: `src/config/env.ts`
@@ -47,6 +52,32 @@ bun install
bun run dev
```
+## Local Drupal server (for testing)
+
+Start local Drupal + MariaDB:
+
+```bash
+docker compose -f docker-compose.local.yml up -d
+```
+
+Then open Drupal installer:
+
+- `http://localhost:8081`
+
+After installation:
+
+1. Enable JSON:API module in Drupal (if not already enabled).
+2. Create at least one Article content item.
+3. Keep frontend env values on proxy mode (`http://localhost:5173` + `/drupal-api/jsonapi`).
+
+Run frontend:
+
+```bash
+bun run dev
+```
+
+Your React app requests `/drupal-api/...` on the Vite dev server, and Vite proxies to Drupal at `http://localhost:8081`, avoiding browser CORS issues.
+
## Docker build and run
Pass Drupal variables at build time (Vite injects `VITE_*` during build):
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
new file mode 100644
index 0000000..fc7b4ad
--- /dev/null
+++ b/docker-compose.local.yml
@@ -0,0 +1,37 @@
+services:
+ drupal:
+ image: drupal:10-apache
+ container_name: portfolio-drupal
+ ports:
+ - '8081:80'
+ environment:
+ DRUPAL_DB_HOST: mariadb
+ DRUPAL_DB_PORT: 3306
+ DRUPAL_DB_NAME: ${DRUPAL_DB_NAME:-drupal}
+ DRUPAL_DB_USER: ${DRUPAL_DB_USER:-drupal}
+ DRUPAL_DB_PASSWORD: ${DRUPAL_DB_PASSWORD:-drupal}
+ depends_on:
+ - mariadb
+ volumes:
+ - drupal-sites:/var/www/html/sites
+ - drupal-modules:/var/www/html/modules
+ - drupal-themes:/var/www/html/themes
+ restart: unless-stopped
+
+ mariadb:
+ image: mariadb:11
+ container_name: portfolio-drupal-db
+ environment:
+ MARIADB_DATABASE: ${DRUPAL_DB_NAME:-drupal}
+ MARIADB_USER: ${DRUPAL_DB_USER:-drupal}
+ MARIADB_PASSWORD: ${DRUPAL_DB_PASSWORD:-drupal}
+ MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root}
+ volumes:
+ - mariadb-data:/var/lib/mysql
+ restart: unless-stopped
+
+volumes:
+ drupal-sites:
+ drupal-modules:
+ drupal-themes:
+ mariadb-data: \ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index aaeba0a..3472fd0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,34 +1,91 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
+import { useEffect, useState } from 'react'
+import { fetchDrupalResource } from './lib/drupalClient'
import './App.css'
+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 [count, setCount] = useState(0)
+ 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()
+ }, [])
return (
- <>
- <div className="flex gap-8 justify-center mb-8">
- <a href="https://vite.dev" target="_blank" rel="noreferrer" className="hover:drop-shadow-lg transition-all">
- <img src={viteLogo} className="h-24 w-24" alt="Vite logo" />
- </a>
- <a href="https://react.dev" target="_blank" rel="noreferrer" className="hover:drop-shadow-lg transition-all">
- <img src={reactLogo} className="h-24 w-24" alt="React logo" />
- </a>
- </div>
- <h1>Vite + React</h1>
- <div className="card">
- <button onClick={() => setCount((count) => count + 1)}>
- count is {count}
- </button>
- <p>
- Edit <code>src/App.tsx</code> and save to test HMR
+ <main>
+ <h1 className="mb-4 text-2xl font-bold">Articles</h1>
+ {!isLoading && !error && resourcePath && <p>Using resource: {resourcePath}</p>}
+
+ {isLoading && <p>Loading articles…</p>}
+
+ {error && (
+ <p role="alert" className="text-red-600">
+ Could not fetch articles: {error}
</p>
- </div>
- <p className="read-the-docs">
- Click on the Vite and React logos to learn more
- </p>
- </>
+ )}
+
+ {!isLoading && !error && articles.length === 0 && (
+ <p>Connected to Drupal, but no articles were returned.</p>
+ )}
+
+ {!isLoading && !error && articles.length > 0 && (
+ <ul style={{ textAlign: 'left' }}>
+ {articles.map((article) => (
+ <li key={article.id}>
+ <strong>{article.attributes?.title ?? '(Untitled)'}</strong>
+ <div>ID: {article.id}</div>
+ <div>Status: {article.attributes?.status ? 'Published' : 'Unpublished'}</div>
+ {article.attributes?.created && <div>Created: {new Date(article.attributes.created).toLocaleString()}</div>}
+ </li>
+ ))}
+ </ul>
+ )}
+ </main>
)
}
diff --git a/vite.config.ts b/vite.config.ts
index 149038b..32cbb6c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -12,4 +12,13 @@ export default defineConfig({
}),
tailwindcss(),
],
+ server: {
+ proxy: {
+ '/drupal-api': {
+ target: 'http://localhost:8081',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/drupal-api/, ''),
+ },
+ },
+ },
})