diff options
| -rw-r--r-- | .env.example | 4 | ||||
| -rw-r--r-- | README.md | 31 | ||||
| -rw-r--r-- | docker-compose.local.yml | 37 | ||||
| -rw-r--r-- | src/App.tsx | 107 | ||||
| -rw-r--r-- | vite.config.ts | 9 |
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= @@ -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/, ''), + }, + }, + }, }) |
