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 00000000..718d6fea Binary files /dev/null and b/src/frontend/app/favicon.ico differ 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 + +