bytes.cat

La wiki d'FP d'informàtica

Eines de l'usuari

Eines del lloc


django_react

Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.

Enllaç a la visualització de la comparació

Ambdós costats versió prèvia Revisió prèvia
Següent revisió
Revisió prèvia
django_react [2026/04/09 15:59]
enric_mieza_sanchez [Creació projecte base Django + ReactJS]
django_react [2026/04/14 18:26] (actual)
enric_mieza_sanchez [Django-Environ i el VCS]
Línia 24: Línia 24:
 Com a bona pràctica afegida emprarem el plugin ''django-environ'' per gestionar les credencials del projecte en un arxiu ''.env''. 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:+Creació del projecte [[Django]] juntament amb [[Django Ninja]] per a l'API:
  
   $ python3 -m venv envdj   $ python3 -m venv envdj
-  (envdj) $ pip install django django-environ django-ninja+  (envdj) $ pip install django django-environ django-ninja django-cors-headers
   (envdj) $ mkdir django-react   (envdj) $ mkdir django-react
   (envdj) $ django-admin startproject mysite django-react   (envdj) $ django-admin startproject mysite django-react
Línia 75: Línia 75:
     'corsheaders',     'corsheaders',
     'biblio.apps.BiblioConfig',     'biblio.apps.BiblioConfig',
-    'django.contrib.admin', +    #... 
-    'django.contrib.auth', +
-    'django.contrib.contenttypes', + 
-    'django.contrib.sessions', +MIDDLEWARE = [ 
-    'django.contrib.messages', +    'corsheaders.middleware.CorsMiddleware', 
-    'django.contrib.staticfiles',+    #...
 ] ]
  
Línia 96: Línia 96:
  
 from .models import * from .models import *
- 
  
 class UsuariAdmin(UserAdmin): class UsuariAdmin(UserAdmin):
Línia 105: Línia 104:
     )     )
     readonly_fields = ["auth_token",]     readonly_fields = ["auth_token",]
- 
  
 admin.site.register(Llibre) admin.site.register(Llibre)
Línia 117: Línia 115:
 \\ \\
  
-==== Projecte ReactJS ====+==== API amb Django Ninja ==== 
 + 
 +Per acabar de configurar la API amb [[Django Ninja]] caldrà modificar ''urls.py'' i afegir ''api.py'': 
 + 
 +<file python urls.py> 
 +#... 
 +from biblio.api import api 
 + 
 +urlpatterns = [ 
 +    #... , 
 +    path("api/", api.urls), 
 +
 +</file> 
 + 
 +Afegir nou arxiu ''api.py''. Si voleu afegir autenticació per protegir els //endpoints//, consultar [[Django Ninja]]. 
 + 
 +<file python api,py> 
 +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 
 +</file> 
 + 
 +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: 
 + 
 +<file .gitignore> 
 +*.pyc 
 +/env/ 
 +/venv/ 
 +/static/* 
 +/media/* 
 +.env 
 +db.sqlite3 
 +.coverage 
 +__pycache__ 
 +</file> 
 + 
 +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: 
 + 
 +<file python settings.py> 
 +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"
 +</file> 
 + 
 +I després crearem ''.env'' amb aquests continguts: 
 +<file bash .env> 
 +DEBUG=on 
 +SECRET_KEY=your-secret-key 
 +#DATABASE_URL=mysql://user:[email protected]: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 
 +</file> 
 + 
 +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: 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   $ npm create vite@latest react
  
-Ja podem començar a fer codi!+ 
 +==== 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 
 + 
 +<file javascript App.jsx> 
 +import { useState } from 'react'; 
 +import './App.css'; 
 +import BookList from './components/BookList'; 
 +import './styles.css'; 
 +import './Modal.css'; 
 + 
 +function App() { 
 +  return ( 
 +    <div className="App"> 
 +      <BookList /> 
 +    </div> 
 +  ); 
 +
 + 
 +export default App; 
 +</file> 
 + 
 +<file javascript config.js> 
 +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]; 
 + 
 +</file> 
 + 
 +<file javascript services/api.js> 
 +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 []; 
 +    }); 
 +}; 
 +</file> 
 + 
 +<file javascript components/BookList.jsx> 
 +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 ( 
 +    <div className="container"> 
 +      <img src={imgReact} /> 
 +      <h1>Llistat de llibres</h1> 
 +      {books.length > 0 ? ( 
 +        books.map((book) => <BookItem key={book.id} book={book} />) 
 +      ) : ( 
 +        <p>Encara no hi ha llibres...</p> 
 +      )} 
 +    </div> 
 +  ); 
 +
 + 
 +export default BookList; 
 +</file> 
 + 
 +<file javascript components/BookItem.jsx> 
 +import React, { useState } from 'react'; 
 + 
 +function BookItem({ book }) { 
 +  const [modalObert, setModalObert] = useState(false); 
 + 
 +  function mostraDetalls() { 
 +    setModalObert(true); 
 +  } 
 + 
 +  function tancaModal() { 
 +    setModalObert(false); 
 +  } 
 + 
 +  return ( 
 +    <div className="book-card"> 
 +      <h3>{book.titol}</h3> 
 +      <p> 
 +        <strong>Autor:</strong> {book.autor} 
 +      </p> 
 +      <button onClick={mostraDetalls}>Detalls</button> 
 + 
 +      {/* Modal */} 
 +      {modalObert && ( 
 +        <div className="modal-overlay" onClick={tancaModal}> 
 +          <div className="modal-content" onClick={e => e.stopPropagation()}> 
 +            <div className="modal-header"> 
 +              <h2>{book.titol}</h2> 
 +              <button className="close-btn" onClick={tancaModal}>×</button> 
 +            </div> 
 +            <div className="modal-body"> 
 +              <p><strong>Autor:</strong> {book.autor}</p> 
 +              <p><strong>Data d'edició:</strong> {book.data_edicio || 'No disponible'}</p> 
 +              <p><strong>Gènere:</strong> {book.genere || 'No disponible'}</p> 
 +              <p><strong>Resum:</strong> {book.resum || 'Sense descripció'}</p> 
 +              {/* Afegeix més camps segons les propietats del teu objecte book */} 
 +            </div> 
 +            <div className="modal-footer"> 
 +              <button onClick={tancaModal}>Tancar</button> 
 +            </div> 
 +          </div> 
 +        </div> 
 +      )} 
 +    </div> 
 +  ); 
 +
 + 
 +export default BookItem; 
 +</file> 
 + 
 +<--
  
 \\ \\
  
-===== API bàsica amb Django Ninja =====+=== Estils CSS ===
  
-Hem tractat la creació d'APIs a l'article [[Django Ninja]].+--> Proposta de codi CSS
  
-Farem una API molt senzilla per llistar els usuaris que tenim a l'aplicació:+<file css App.css> 
 +#root { 
 +  max-width1280px; 
 +  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;
 +}
 +</file>
 +
 +<file css Modal.css>
 +/* 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;
 +}
 +</file>
 +
 +<file css styles.css>
 +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;
 +}
 +</file>
 +
 +Aquest sol venir a l'esquelet del projecte, sol anar canviant d'una versió a l'altra
 +<file css index.css>
 +: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;
 +  }
 +}
 +</file>
 +
 +<--
 +
 +\\
  
django_react.1775750359.txt.gz · Darrera modificació: 2026/04/09 15:59 per enric_mieza_sanchez