====== Django i React ====== Farem una plantilla de projecte amb [[Django]] i [[React]], i emprarem [[Django Ninja]] com a motor per a l'API. A l'article [[integracio de Django amb React]] podeu veure la versió amb Django REST Framework. {{ django-react.jpg?400 }} {{tag> #FpInfor #Daw #DawMp06 #DawMp07 django framework python react javascript web}} \\ ===== Creació projecte base Django ===== Es podria fer en dos repositoris separats, però per simplicitat ho farem en un sol. Farem un projecte Django i hi posarem la part de React dins la carpeta ''react/''. Per poder treballar amb React necessitarem que Django actui com a API. Utilitzarem l'admin panel per facilitar l'inici del desenvolupament, però no farem servir el frontend del propi Django, delegant tota aquesta part a React. Per a la part de l'API emprarem [[Django Ninja]] en aquest exemple, tot i que també es podria fer servir Django REST Framework ([[Django API]]). ==== Projecte Django ==== Com a bona pràctica afegida emprarem el plugin ''django-environ'' per gestionar les credencials del projecte en un arxiu ''.env''. Creació del projecte [[Django]] juntament amb [[Django Ninja]] per a l'API: $ python3 -m venv envdj (envdj) $ pip install django django-environ django-ninja django-cors-headers (envdj) $ mkdir django-react (envdj) $ django-admin startproject mysite django-react (envdj) $ pip freeze > requirements.txt Penseu a afegir l'arxiu ''.gitignore'' que podeu trobar a l'article [[Django]]. Hi crearem una app de llibres "biblio" molt senzilla amb un sol model de llibre: (envdj) $ ./manage.py startapp biblio Afegim els models de l'app a ''models.py'': from django.db import models from django.contrib.auth.models import AbstractUser class Llibre (models.Model): titol = models.CharField(max_length=100) autor = models.CharField(max_length=200) resum = models.TextField(null=True,blank=True) data_edicio = models.DateField() def __str__(self): return self.titol class Usuari(AbstractUser): auth_token = models.CharField(max_length=32,blank=True,null=True) # + altres atributs que es vulguin afegir... És important fer un usuari personalitzat des de l'inici del projecte. Ho necessitem per implementar la token authentication del [[Django Ninja]] però també per evitar problemes quan necessitem personalitzar el model User mes endavant. Ajustem ''settings.py'' per: * Informar del canvi de model de usuari per ''biblio.User'' * Afegir la nova app ''biblio'' * Afegir el plugin ''corsheaders'' per facilitar les crides amb dominis creuats. * Configurar ''corsheaders'' amb els dominis permesos a''CORS_ALLOWED_ORIGINS''. Afegiu la app al ''settings.py'' i també ''corsheaders'' per a la gestió de l'API: AUTH_USER_MODEL = 'biblio.Usuari' INSTALLED_APPS = [ 'corsheaders', 'biblio.apps.BiblioConfig', #... ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', #... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", # Exemple: React en desenvolupament amb Vite o CRA "http://127.0.0.1:5173", "https://el-teu-domini.com", # Quan el tinguis en producció ] Afegim els models a ''admin.py'': from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import * class UsuariAdmin(UserAdmin): fieldsets = UserAdmin.fieldsets + ( ("Altres dades (API auth)", { 'fields': ('auth_token',), }), ) readonly_fields = ["auth_token",] admin.site.register(Llibre) admin.site.register(Usuari,UsuariAdmin) **Afegeix alguns elements (llibres) des de l'admin panel de Django**, aquest ho permet amb comoditat. Necessitarem alguns exemples per depurar l'app, així que pren-te el temps d'omplir la BD de dades una mica. \\ ==== API amb Django Ninja ==== Per acabar de configurar la API amb [[Django Ninja]] caldrà modificar ''urls.py'' i afegir ''api.py'': #... from biblio.api import api urlpatterns = [ #... , path("api/", api.urls), ] Afegir nou arxiu ''api.py''. Si voleu afegir autenticació per protegir els //endpoints//, consultar [[Django Ninja]]. from ninja import NinjaAPI, Schema from django.shortcuts import get_object_or_404 from typing import List, Optional, Union, Literal import datetime from .models import * api = NinjaAPI() class LlibreOut(Schema): id: int titol: str autor: str data_edicio: datetime.date resum: Optional[str] @api.get("/llibres", response=List[LlibreOut]) @api.get("/llibres/", response=List[LlibreOut]) def obtenir_libres(request): qs = Llibre.objects.all() return qs Si ho heu configurat bé i heu afegit llibres, us hauria de funcionar: $ curl localhost:8000/api/llibres \\ ==== Django-Environ i el VCS ==== [[https://django-environ.readthedocs.io/en/latest/quickstart.html|Django Environ]] és un //plugin// que ens ajuda a la posada en producció. Particularment ens facilitarà configurar de forma segura les credencials per a la BD i altres serveis (social login, seguretat, tokens, accés a APIs externes, etc.). Habitualment les configuracions es posen a l'arxiu ''settings.py'', però a la pràctica hem de treure les dades sensibles perquè no es carreguin al sistema de control de versions (Git habitualment). La primera mesura important és tenir un arxiu ''.gitignore'' adequat, com: *.pyc /env/ /venv/ /static/* /media/* .env db.sqlite3 .coverage __pycache__ Com podeu veure, hi ha l'arxiu ''.env'' que és el que ha de tenir les credencials. Si està a ''.gitignore'', no s'afegirà al repo quan fem instruccions com ''git add .'' Si mirem el [[https://django-environ.readthedocs.io/en/latest/quickstart.html|quickstart de django-environ]] veureu que la idea és treure les variables de ''settings.py'' a ''.env'', en particular: import environ import os env = environ.Env( DEBUG=(bool, False) ) # Podeu deixar les instruccions que hi hagi de l'esquelet de Django BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # llegim .env environ.Env.read_env(os.path.join(BASE_DIR, '.env')) # variables a llegir de .env # ULL! cal elimninar les variables que hi ha al settings.py DEBUG = env('DEBUG') SECRET_KEY = env('SECRET_KEY') ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*",]) DATABASES = { # configura a través de la variable DATABASE_URL 'default': env.db(), } # dominis amb autorització per a fer crides a l'API CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS",default=[ "http://localhost:5173", # Exemple: React en desenvolupament amb Vite o CRA "http://127.0.0.1:5173", ]) # protecció CSRF per atacs de dominis creuats CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS") I després crearem ''.env'' amb aquests continguts: DEBUG=on SECRET_KEY=your-secret-key #DATABASE_URL=mysql://user:un-githubbedpassword@127.0.0.1:3306/biblio DATABASE_URL=sqlite:///db.sqlite3 ALLOWED_HOSTS=*,elmeudomini.com CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,https://elmeudomini.com CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,https://elmeudomini.com Sempre convé deixar aquest exemple mateix guardat com a ''.env.example'' per tenir una plantilla. Aquest sí que el podem pujar a github sense problema. \\ ===== Projecte ReactJS ===== Creació del projecte React dins la carpeta ''react/'' del projecte Django amb Vite. Caldrà triar projecte React + JavaScript: Ens hem de posar a la carpeta principal del projecte Django, al mateix nivell que el ''manage.py''. $ npm create vite@latest react ==== Codi de l'aplicació ReactJS ==== Els arxius de codi seran: * App.jsx * config.js * services/api.js * components/BookList.jsx * components/BookItem.jsx I els CSS: * App.css * Modal.css * styles.css * index.css === Javascript i JSX === --> Proposta de codi JSX import { useState } from 'react'; import './App.css'; import BookList from './components/BookList'; import './styles.css'; import './Modal.css'; function App() { return (
); } export default App;
const config = { development: { API_URL: 'http://localhost:8000/api', DEBUG: true, }, production: { API_URL: 'https://elmeudomini.com/api', DEBUG: false, } }; const env = process.env.NODE_ENV || 'development'; export default config[env]; import config from '../config'; const API_BASE_URL = config.API_URL; export const getBooks = () => { console.log('cridant API...'); return fetch(API_BASE_URL+"/llibres") .then((response) => { if (!response.ok) { throw new Error("Error l'obtenir els llibres"); } return response.json(); }) .catch((error) => { console.error('Error en la API:', error); return []; }); }; import { useEffect, useState } from 'react'; import { getBooks } from '../services/api'; import BookItem from './BookItem'; import imgReact from '../assets/react.svg'; function BookList() { const [books, setBooks] = useState([]); useEffect(() => { getBooks().then((data) => setBooks(data)); }, []); return (

Llistat de llibres

{books.length > 0 ? ( books.map((book) => ) ) : (

Encara no hi ha llibres...

)}
); } export default BookList;
import React, { useState } from 'react'; function BookItem({ book }) { const [modalObert, setModalObert] = useState(false); function mostraDetalls() { setModalObert(true); } function tancaModal() { setModalObert(false); } return (

{book.titol}

Autor: {book.autor}

{/* Modal */} {modalObert && (
e.stopPropagation()}>

{book.titol}

Autor: {book.autor}

Data d'edició: {book.data_edicio || 'No disponible'}

Gènere: {book.genere || 'No disponible'}

Resum: {book.resum || 'Sense descripció'}

{/* Afegeix més camps segons les propietats del teu objecte book */}
)}
); } export default BookItem;
<-- \\ === Estils CSS === --> Proposta de codi CSS #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } /* Modal.css */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background-color: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 10px; } .modal-header h2 { margin: 0; font-size: 1.5rem; } .close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .close-btn:hover { color: #000; } .modal-body { margin-bottom: 20px; line-height: 1.6; } .modal-body p { margin: 10px 0; } .modal-footer { display: flex; justify-content: flex-end; border-top: 1px solid #eee; padding-top: 10px; } .modal-footer button { padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .modal-footer button:hover { background-color: #0056b3; } body { font-family: Arial, sans-serif; background-color: #f5f5f5; text-align: center; } .container { max-width: 800px; margin: 20px auto; } .book-card { background: white; padding: 15px; border-radius: 5px; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); margin-bottom: 15px; text-align: left; } h1 { color: #333; } h3 { color: #007bff; } h4 { margin-top: 10px; } ul { list-style: none; padding: 0; } ul li { background: #e9ecef; padding: 5px; margin: 5px 0; border-radius: 3px; } p { color: black; } Aquest sol venir a l'esquelet del projecte, sol anar canviant d'una versió a l'altra :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } <-- \\