From 312a680b6601816749dcff82c72b734346d3fcc6 Mon Sep 17 00:00:00 2001 From: Lebaud Antoine Date: Wed, 10 Jan 2024 15:07:05 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20set=20up=20Vite-based=20f?= =?UTF-8?q?rontend=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chose Vite for static output efficiency, aligning with project needs. All API interactions are currently unauthenticated. SSO support planned soon, using ProConnect. UX is minimalistic, and showcases the core idea. Components introduced: * AppProvider * Select and TextArea Rhf inputs API hooks introduced: * useGeneratePDF, generates a PDF, and downloads it in the client. * useTemplates, fetches available templates to populate Select options. --- src/backend/core/models.py | 2 +- src/backend/core/urls.py | 4 +- src/backend/core/views.py | 52 + src/backend/publish/settings.py | 2 +- src/frontend/.eslintrc.cjs | 18 + src/frontend/app/favicon.ico | Bin 0 -> 25931 bytes src/frontend/app/globals.css | 76 + src/frontend/app/layout.tsx | 22 + src/frontend/app/page.tsx | 157 + src/frontend/components/ui/button.tsx | 56 + src/frontend/components/ui/card.tsx | 79 + src/frontend/components/ui/form.tsx | 176 + src/frontend/components/ui/input.tsx | 25 + src/frontend/components/ui/label.tsx | 24 + src/frontend/components/ui/select.tsx | 158 + src/frontend/components/ui/sonner.tsx | 29 + src/frontend/components/ui/textarea.tsx | 24 + src/frontend/index.html | 13 + src/frontend/package-lock.json | 5481 +++++++++++++++++ src/frontend/package.json | 38 + src/frontend/public/next.svg | 1 + src/frontend/public/vercel.svg | 1 + src/frontend/public/vite.svg | 1 + src/frontend/src/.env.development | 0 src/frontend/src/App.scss | 22 + src/frontend/src/App.tsx | 90 + src/frontend/src/api/index.ts | 2 + src/frontend/src/api/useGeneratePDF.ts | 28 + src/frontend/src/api/useTemplates.ts | 23 + .../src/components/AppProvider/index.tsx | 17 + .../src/components/Form/Select/index.tsx | 24 + .../src/components/Form/TextArea/index.tsx | 26 + src/frontend/src/components/Form/index.ts | 2 + src/frontend/src/components/index.ts | 2 + src/frontend/src/cunningham-tokens.css | 163 + src/frontend/src/index.scss | 30 + src/frontend/src/main.tsx | 13 + src/frontend/src/vite-env.d.ts | 7 + src/frontend/tsconfig.json | 25 + src/frontend/tsconfig.node.json | 10 + src/frontend/vite.config.ts | 6 + 41 files changed, 6926 insertions(+), 3 deletions(-) create mode 100644 src/frontend/.eslintrc.cjs create mode 100644 src/frontend/app/favicon.ico create mode 100644 src/frontend/app/globals.css create mode 100644 src/frontend/app/layout.tsx create mode 100644 src/frontend/app/page.tsx create mode 100644 src/frontend/components/ui/button.tsx create mode 100644 src/frontend/components/ui/card.tsx create mode 100644 src/frontend/components/ui/form.tsx create mode 100644 src/frontend/components/ui/input.tsx create mode 100644 src/frontend/components/ui/label.tsx create mode 100644 src/frontend/components/ui/select.tsx create mode 100644 src/frontend/components/ui/sonner.tsx create mode 100644 src/frontend/components/ui/textarea.tsx create mode 100644 src/frontend/index.html create mode 100644 src/frontend/package-lock.json create mode 100644 src/frontend/package.json create mode 100644 src/frontend/public/next.svg create mode 100644 src/frontend/public/vercel.svg create mode 100644 src/frontend/public/vite.svg create mode 100644 src/frontend/src/.env.development create mode 100644 src/frontend/src/App.scss create mode 100644 src/frontend/src/App.tsx create mode 100644 src/frontend/src/api/index.ts create mode 100644 src/frontend/src/api/useGeneratePDF.ts create mode 100644 src/frontend/src/api/useTemplates.ts create mode 100644 src/frontend/src/components/AppProvider/index.tsx create mode 100644 src/frontend/src/components/Form/Select/index.tsx create mode 100644 src/frontend/src/components/Form/TextArea/index.tsx create mode 100644 src/frontend/src/components/Form/index.ts create mode 100644 src/frontend/src/components/index.ts create mode 100644 src/frontend/src/cunningham-tokens.css create mode 100644 src/frontend/src/index.scss create mode 100644 src/frontend/src/main.tsx create mode 100644 src/frontend/src/vite-env.d.ts create mode 100644 src/frontend/tsconfig.json create mode 100644 src/frontend/tsconfig.node.json create mode 100644 src/frontend/vite.config.ts diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 63dbcba8..8202c2db 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -236,7 +236,7 @@ class Template(BaseModel): Generate and return a PDF document for this template around the markdown body passed as argument. """ - body_html = markdown.markdown(textwrap.dedent(body)) if body else "" + body_html = markdown.markdown(textwrap.dedent(body)) if body else "" document_html = HTML(string=DjangoTemplate(self.code).render(Context({"body": body_html}))) css = CSS( string=self.css, diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index 67421878..7be45b56 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -1,8 +1,10 @@ """URL configuration for the core app.""" from django.urls import path -from core.views import generate_document +from core.views import generate_document, TemplatesApiView, GenerateDocumentAPIView urlpatterns = [ path('generate-document/', generate_document, name='generate_document'), + path('api/generate-document/', GenerateDocumentAPIView.as_view(), name='generate-document'), + path('api/templates', TemplatesApiView.as_view()), ] diff --git a/src/backend/core/views.py b/src/backend/core/views.py index ac229f81..bbc37a11 100644 --- a/src/backend/core/views.py +++ b/src/backend/core/views.py @@ -1,6 +1,14 @@ from django.shortcuts import render, HttpResponse from .forms import DocumentGenerationForm from .models import Template +from rest_framework import status + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import serializers + +from django.http import FileResponse +from io import BytesIO def generate_document(request): @@ -25,3 +33,47 @@ def generate_document(request): return render(request, 'core/generate_document.html', {'form': form}) + +class DocumentGenerationSerializer(serializers.Serializer): + body = serializers.CharField(label="Markdown Body") + template_id = serializers.UUIDField(format='hex_verbose') + +class GenerateDocumentAPIView(APIView): + def post(self, request): + serializer = DocumentGenerationSerializer(data=request.data) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + template_id = serializer.validated_data['template_id'] + body = serializer.validated_data['body'] + + try: + template = Template.objects.get(pk=template_id) + except Template.DoesNotExist: + return Response("Template not found", status=status.HTTP_404_NOT_FOUND) + + pdf_content = template.generate_document(body) + + response = FileResponse(BytesIO(pdf_content), content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename={template.title}.pdf' + return response + + + +class TemplateSerializer(serializers.ModelSerializer): + class Meta: + model = Template + fields = ['id', 'title'] + +class TemplatesApiView(APIView): + """Wip.""" + + def get(self, request, *args, **kwargs): + """Wip.""" + templates = Template.objects.all() + serializer = TemplateSerializer(templates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + + diff --git a/src/backend/publish/settings.py b/src/backend/publish/settings.py index 83330891..5c8ae0be 100755 --- a/src/backend/publish/settings.py +++ b/src/backend/publish/settings.py @@ -270,7 +270,7 @@ class Base(Configuration): # CORS CORS_ALLOW_CREDENTIALS = True - CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False) + CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True) CORS_ALLOWED_ORIGINS = values.ListValue([]) CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([]) diff --git a/src/frontend/.eslintrc.cjs b/src/frontend/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/src/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/src/frontend/app/favicon.ico b/src/frontend/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/src/frontend/app/globals.css b/src/frontend/app/globals.css new file mode 100644 index 00000000..52a8d2d5 --- /dev/null +++ b/src/frontend/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/src/frontend/app/layout.tsx b/src/frontend/app/layout.tsx new file mode 100644 index 00000000..40e027fb --- /dev/null +++ b/src/frontend/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/src/frontend/app/page.tsx b/src/frontend/app/page.tsx new file mode 100644 index 00000000..05a0803b --- /dev/null +++ b/src/frontend/app/page.tsx @@ -0,0 +1,157 @@ +'use client'; + +import Image from 'next/image' +import {Button} from "@/components/ui/button"; +import React, {useEffect, useState} from "react"; +import {Textarea} from "@/components/ui/textarea"; +import {Input} from "@/components/ui/input"; +import {Form} from "@/components/ui/form"; + + +import * as z from "zod" +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import {Toaster} from "@/components/ui/sonner"; +import {toast} from "sonner"; + +const formSchema = z.object({ + body: z.string(), + template_id: z.string() +}) + +export default function Home() { + + + const [templates, setTemplates] = useState([]); + const [isFetching, setIsFetching] = useState(false); + + const fetchTemplates = async () => { + const res= await fetch('http://localhost:8071/api/templates'); + if (!res.ok) { + // This will activate the closest `error.js` Error Boundary + throw new Error('Failed to fetch data') + } + const templates = await res.json() + setTemplates(templates); + }; + + useEffect( () => { + fetchTemplates(); + }, []) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + body: "", + template_id: "", + }, + }) + + function download(blob, filename) { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + // the filename you want + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } + + const generateDocument = async (values: any) => { + const res = await fetch('http://localhost:8071/api/generate-document/', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(values), + }) + + if (!res.ok) { + // This will activate the closest `error.js` Error Boundary + throw new Error('Failed to generate document') + } + + return await res.blob() + } + + async function onSubmit(values: z.infer) { + // Do something with the form values. + // ✅ This will be type-safe and validated. + setIsFetching(true) + + try { + const document = await generateDocument(values) + download(document, "wip.pdf") + + toast("Fichier téléchargé.", { + description: "Nous avons généré votre document à partir du template sélectionné.", + }) + setIsFetching(false) + + } catch (e) { + setIsFetching(false) + } + } + + return ( +
+
+ + + + Imprint +
+
+
+ + ( + + Contenu + +