✨(frontend) set up Vite-based frontend project
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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([])
|
||||
|
||||
|
||||
18
src/frontend/.eslintrc.cjs
Normal file
18
src/frontend/.eslintrc.cjs
Normal file
@@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
BIN
src/frontend/app/favicon.ico
Normal file
BIN
src/frontend/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
76
src/frontend/app/globals.css
Normal file
76
src/frontend/app/globals.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
22
src/frontend/app/layout.tsx
Normal file
22
src/frontend/app/layout.tsx
Normal file
@@ -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 (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
157
src/frontend/app/page.tsx
Normal file
157
src/frontend/app/page.tsx
Normal file
@@ -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<z.infer<typeof formSchema>>({
|
||||
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<typeof formSchema>) {
|
||||
// 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 (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24 relative">
|
||||
<div className="absolute left-8 top-8 flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 mr-2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" />
|
||||
</svg>
|
||||
<span className="font-medium">Imprint</span>
|
||||
</div>
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="body"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Contenu</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Coller votre code markdown." {...field} className="min-h-96" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="template_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Template</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionner un model de document." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{
|
||||
templates?.map((template) => (
|
||||
<SelectItem value={template.id} key={template.id}>{template.title}</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={isFetching}>Générer votre PDF</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<Toaster />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
56
src/frontend/components/ui/button.tsx
Normal file
56
src/frontend/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
src/frontend/components/ui/card.tsx
Normal file
79
src/frontend/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
176
src/frontend/components/ui/form.tsx
Normal file
176
src/frontend/components/ui/form.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
25
src/frontend/components/ui/input.tsx
Normal file
25
src/frontend/components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
24
src/frontend/components/ui/label.tsx
Normal file
24
src/frontend/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
158
src/frontend/components/ui/select.tsx
Normal file
158
src/frontend/components/ui/select.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
29
src/frontend/components/ui/sonner.tsx
Normal file
29
src/frontend/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
24
src/frontend/components/ui/textarea.tsx
Normal file
24
src/frontend/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
13
src/frontend/index.html
Normal file
13
src/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Imprint</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5481
src/frontend/package-lock.json
generated
Normal file
5481
src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/frontend/package.json
Normal file
38
src/frontend/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"build-theme": "cunningham -g css -o src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "3.3.4",
|
||||
"@openfun/cunningham-react": "2.4.0",
|
||||
"@tanstack/react-query": "5.17.10",
|
||||
"@tanstack/react-query-devtools": "5.17.10",
|
||||
"axios": "1.6.5",
|
||||
"downloadjs": "1.4.7",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "7.49.3",
|
||||
"yup": "1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.2.47",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitejs/plugin-react": "4.2.1",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-refresh": "0.4.5",
|
||||
"sass": "1.69.7",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.11"
|
||||
}
|
||||
}
|
||||
1
src/frontend/public/next.svg
Normal file
1
src/frontend/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/frontend/public/vercel.svg
Normal file
1
src/frontend/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
1
src/frontend/public/vite.svg
Normal file
1
src/frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
0
src/frontend/src/.env.development
Normal file
0
src/frontend/src/.env.development
Normal file
22
src/frontend/src/App.scss
Normal file
22
src/frontend/src/App.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
.c__app {
|
||||
|
||||
&__title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
$gap: 1rem;
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 55rem;
|
||||
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($gap / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/frontend/src/App.tsx
Normal file
90
src/frontend/src/App.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {useEffect, useMemo} from 'react'
|
||||
import './App.scss'
|
||||
import {FormProvider, useForm} from "react-hook-form";
|
||||
import {Button} from "@openfun/cunningham-react";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import {TextArea, Select} from "./components";
|
||||
import {useTemplates, useGeneratePDF} from "./api";
|
||||
|
||||
interface FormValues {
|
||||
body: string;
|
||||
template_id: string;
|
||||
}
|
||||
|
||||
const FormSchema = Yup.object().shape({
|
||||
body: Yup.string().required('Veuillez saisir votre contenu markdown.'),
|
||||
template_id: Yup.string().required('Veuillez sélectionner un template.'),
|
||||
});
|
||||
|
||||
export interface FormProps {
|
||||
values?: FormValues;
|
||||
}
|
||||
|
||||
function App({ values }: FormProps) {
|
||||
|
||||
const methods = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
body: "",
|
||||
template_id: ""
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
resolver: yupResolver(FormSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
methods.reset(values);
|
||||
}, [values, methods]);
|
||||
|
||||
|
||||
const { error, data: templates, isFetching } = useTemplates()
|
||||
const { mutate: generatePDF } = useGeneratePDF();
|
||||
|
||||
const options = useMemo(() => {
|
||||
|
||||
if (!templates) return [];
|
||||
|
||||
return templates?.map((template) => ({
|
||||
label: template.title,
|
||||
value: template.id
|
||||
}));
|
||||
|
||||
}, [templates])
|
||||
|
||||
if (isFetching) return <div>Loading...</div>
|
||||
if (error) return <div>Something went wrong...</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="c__app__title">Imprint</h1>
|
||||
<div>
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className="c__app__form"
|
||||
onSubmit={methods.handleSubmit((values) => generatePDF({data:values, filename:"wipp.pdf"}))}
|
||||
>
|
||||
<div className="c__app__form__inputs">
|
||||
<TextArea
|
||||
name="body"
|
||||
label="Saisir votre contenu markdown"
|
||||
fullWidth={true}
|
||||
style={{minHeight: "24rem"}}
|
||||
/>
|
||||
<Select
|
||||
name="template_id"
|
||||
label="Sélectionner un template"
|
||||
fullWidth={true}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
<Button fullWidth={true}>Générer votre PDF</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
2
src/frontend/src/api/index.ts
Normal file
2
src/frontend/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./useTemplates"
|
||||
export * from "./useGeneratePDF"
|
||||
28
src/frontend/src/api/useGeneratePDF.ts
Normal file
28
src/frontend/src/api/useGeneratePDF.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {axios} from "../lib";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import download from 'downloadjs';
|
||||
|
||||
|
||||
interface PDF {
|
||||
data: {
|
||||
template_id: string;
|
||||
body: string;
|
||||
};
|
||||
filename: string;
|
||||
}
|
||||
|
||||
const generatePDF = async ({data, filename}: PDF) => {
|
||||
const response = await axios.post('generate-document/', data, {responseType: 'blob'});
|
||||
const content = response.headers['content-type'];
|
||||
return {data: response.data, filename, content}
|
||||
}
|
||||
|
||||
|
||||
export const useGeneratePDF = () => {
|
||||
return useMutation({
|
||||
mutationFn: generatePDF,
|
||||
onSuccess: ({data, filename, content}) => {
|
||||
download(data, filename, content)
|
||||
},
|
||||
});
|
||||
}
|
||||
23
src/frontend/src/api/useTemplates.ts
Normal file
23
src/frontend/src/api/useTemplates.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {axios} from "../lib";
|
||||
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
type TemplatesResponse = Array<Template>;
|
||||
|
||||
export const getTemplates = async () : Promise<TemplatesResponse> => {
|
||||
const response = await axios.get('templates')
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const useTemplates = () => {
|
||||
return useQuery({
|
||||
queryKey: ['templates'],
|
||||
queryFn: () => getTemplates(),
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
17
src/frontend/src/components/AppProvider/index.tsx
Normal file
17
src/frontend/src/components/AppProvider/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import {QueryClientProvider} from "@tanstack/react-query";
|
||||
import {CunninghamProvider} from "@openfun/cunningham-react";
|
||||
import {queryClient} from "../../lib";
|
||||
import {ReactQueryDevtools} from "@tanstack/react-query-devtools";
|
||||
|
||||
export type AppProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AppProvider = ({children}: AppProviderProps) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen />
|
||||
</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
24
src/frontend/src/components/Form/Select/index.tsx
Normal file
24
src/frontend/src/components/Form/Select/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Select as CSelect, SelectProps as CSelectProps} from "@openfun/cunningham-react";
|
||||
import {Controller, useFormContext} from "react-hook-form";
|
||||
|
||||
export const Select = (props: CSelectProps & { name: string }) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={props.name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<CSelect
|
||||
{...props}
|
||||
state={fieldState.error ? "error" : "default"}
|
||||
text={fieldState.error?.message}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(e) => setValue(field.name, e.target.value)}
|
||||
value={field.value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
26
src/frontend/src/components/Form/TextArea/index.tsx
Normal file
26
src/frontend/src/components/Form/TextArea/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import {TextAreaProps as CTextAreaProps, TextArea as CTextArea} from "@openfun/cunningham-react";
|
||||
import {Controller, useFormContext} from "react-hook-form";
|
||||
|
||||
export const TextArea = (props: CTextAreaProps & { name: string }) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={props.name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<CTextArea
|
||||
{...props}
|
||||
aria-invalid={!!fieldState.error}
|
||||
state={fieldState.error ? "error" : "default"}
|
||||
text={fieldState.error?.message}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(e) => setValue(field.name, e.target.value)}
|
||||
value={field.value}
|
||||
fullWidth={true}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
2
src/frontend/src/components/Form/index.ts
Normal file
2
src/frontend/src/components/Form/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TextArea"
|
||||
export * from "./Select"
|
||||
2
src/frontend/src/components/index.ts
Normal file
2
src/frontend/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Form'
|
||||
export * from './AppProvider'
|
||||
163
src/frontend/src/cunningham-tokens.css
Normal file
163
src/frontend/src/cunningham-tokens.css
Normal file
@@ -0,0 +1,163 @@
|
||||
:root {
|
||||
--c--theme--colors--secondary-text: var(--c--theme--colors--greyscale-700);
|
||||
--c--theme--colors--secondary-100: #F2F7FC;
|
||||
--c--theme--colors--secondary-200: #EBF3FA;
|
||||
--c--theme--colors--secondary-300: #E2EEF8;
|
||||
--c--theme--colors--secondary-400: #DDEAF7;
|
||||
--c--theme--colors--secondary-500: #D4E5F5;
|
||||
--c--theme--colors--secondary-600: #C1D0DF;
|
||||
--c--theme--colors--secondary-700: #97A3AE;
|
||||
--c--theme--colors--secondary-800: #757E87;
|
||||
--c--theme--colors--secondary-900: #596067;
|
||||
--c--theme--colors--info-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--info-100: #EBF2FC;
|
||||
--c--theme--colors--info-200: #8CB5EA;
|
||||
--c--theme--colors--info-300: #5894E1;
|
||||
--c--theme--colors--info-400: #377FDB;
|
||||
--c--theme--colors--info-500: #055FD2;
|
||||
--c--theme--colors--info-600: #0556BF;
|
||||
--c--theme--colors--info-700: #044395;
|
||||
--c--theme--colors--info-800: #033474;
|
||||
--c--theme--colors--info-900: #022858;
|
||||
--c--theme--colors--greyscale-100: #FAFAFB;
|
||||
--c--theme--colors--greyscale-200: #F3F4F4;
|
||||
--c--theme--colors--greyscale-300: #E7E8EA;
|
||||
--c--theme--colors--greyscale-400: #C2C6CA;
|
||||
--c--theme--colors--greyscale-500: #9EA3AA;
|
||||
--c--theme--colors--greyscale-600: #79818A;
|
||||
--c--theme--colors--greyscale-700: #555F6B;
|
||||
--c--theme--colors--greyscale-800: #303C4B;
|
||||
--c--theme--colors--greyscale-900: #0C1A2B;
|
||||
--c--theme--colors--greyscale-000: #FFFFFF;
|
||||
--c--theme--colors--primary-100: #EBF2FC;
|
||||
--c--theme--colors--primary-200: #8CB5EA;
|
||||
--c--theme--colors--primary-300: #5894E1;
|
||||
--c--theme--colors--primary-400: #377FDB;
|
||||
--c--theme--colors--primary-500: #055FD2;
|
||||
--c--theme--colors--primary-600: #0556BF;
|
||||
--c--theme--colors--primary-700: #044395;
|
||||
--c--theme--colors--primary-800: #033474;
|
||||
--c--theme--colors--primary-900: #022858;
|
||||
--c--theme--colors--success-100: #EFFCD3;
|
||||
--c--theme--colors--success-200: #DBFAA9;
|
||||
--c--theme--colors--success-300: #BEF27C;
|
||||
--c--theme--colors--success-400: #A0E659;
|
||||
--c--theme--colors--success-500: #76D628;
|
||||
--c--theme--colors--success-600: #5AB81D;
|
||||
--c--theme--colors--success-700: #419A14;
|
||||
--c--theme--colors--success-800: #2C7C0C;
|
||||
--c--theme--colors--success-900: #1D6607;
|
||||
--c--theme--colors--warning-100: #FFF8CD;
|
||||
--c--theme--colors--warning-200: #FFEF9B;
|
||||
--c--theme--colors--warning-300: #FFE469;
|
||||
--c--theme--colors--warning-400: #FFDA43;
|
||||
--c--theme--colors--warning-500: #FFC805;
|
||||
--c--theme--colors--warning-600: #DBA603;
|
||||
--c--theme--colors--warning-700: #B78702;
|
||||
--c--theme--colors--warning-800: #936901;
|
||||
--c--theme--colors--warning-900: #7A5400;
|
||||
--c--theme--colors--danger-100: #F4B0B0;
|
||||
--c--theme--colors--danger-200: #EE8A8A;
|
||||
--c--theme--colors--danger-300: #E65454;
|
||||
--c--theme--colors--danger-400: #E13333;
|
||||
--c--theme--colors--danger-500: #DA0000;
|
||||
--c--theme--colors--danger-600: #C60000;
|
||||
--c--theme--colors--danger-700: #9B0000;
|
||||
--c--theme--colors--danger-800: #780000;
|
||||
--c--theme--colors--danger-900: #5C0000;
|
||||
--c--theme--colors--primary-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--success-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--warning-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--font--sizes--h1: 1.75rem;
|
||||
--c--theme--font--sizes--h2: 1.375rem;
|
||||
--c--theme--font--sizes--h3: 1.125rem;
|
||||
--c--theme--font--sizes--h4: 0.8125rem;
|
||||
--c--theme--font--sizes--h5: 0.625rem;
|
||||
--c--theme--font--sizes--h6: 0.5rem;
|
||||
--c--theme--font--sizes--l: 1rem;
|
||||
--c--theme--font--sizes--m: 0.8125rem;
|
||||
--c--theme--font--sizes--s: 0.6875rem;
|
||||
--c--theme--font--weights--thin: 200;
|
||||
--c--theme--font--weights--light: 300;
|
||||
--c--theme--font--weights--regular: 400;
|
||||
--c--theme--font--weights--medium: 500;
|
||||
--c--theme--font--weights--bold: 600;
|
||||
--c--theme--font--weights--extrabold: 700;
|
||||
--c--theme--font--weights--black: 800;
|
||||
--c--theme--font--families--base: "Roboto Flex Variable", sans-serif;
|
||||
--c--theme--font--families--accent: "Roboto Flex Variable", sans-serif;
|
||||
--c--theme--font--letterspacings--h1: normal;
|
||||
--c--theme--font--letterspacings--h2: normal;
|
||||
--c--theme--font--letterspacings--h3: normal;
|
||||
--c--theme--font--letterspacings--h4: normal;
|
||||
--c--theme--font--letterspacings--h5: 1px;
|
||||
--c--theme--font--letterspacings--h6: normal;
|
||||
--c--theme--font--letterspacings--l: normal;
|
||||
--c--theme--font--letterspacings--m: normal;
|
||||
--c--theme--font--letterspacings--s: normal;
|
||||
--c--theme--spacings--xl: 4rem;
|
||||
--c--theme--spacings--l: 3rem;
|
||||
--c--theme--spacings--b: 1.625rem;
|
||||
--c--theme--spacings--s: 1rem;
|
||||
--c--theme--spacings--t: 0.5rem;
|
||||
--c--theme--spacings--st: 0.25rem;
|
||||
--c--theme--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0);
|
||||
--c--theme--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--c--theme--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--c--theme--transitions--duration: 250ms;
|
||||
--c--theme--breakpoints--xs: 0;
|
||||
--c--theme--breakpoints--sm: 576px;
|
||||
--c--theme--breakpoints--md: 768px;
|
||||
--c--theme--breakpoints--lg: 992px;
|
||||
--c--theme--breakpoints--xl: 1200px;
|
||||
--c--theme--breakpoints--xxl: 1400px;
|
||||
}
|
||||
.cunningham-theme--dark{
|
||||
--c--theme--colors--greyscale-100: #182536;
|
||||
--c--theme--colors--greyscale-200: #303C4B;
|
||||
--c--theme--colors--greyscale-300: #555F6B;
|
||||
--c--theme--colors--greyscale-400: #79818A;
|
||||
--c--theme--colors--greyscale-500: #9EA3AA;
|
||||
--c--theme--colors--greyscale-600: #C2C6CA;
|
||||
--c--theme--colors--greyscale-700: #E7E8EA;
|
||||
--c--theme--colors--greyscale-800: #F3F4F4;
|
||||
--c--theme--colors--greyscale-900: #FAFAFB;
|
||||
--c--theme--colors--greyscale-000: #0C1A2B;
|
||||
--c--theme--colors--primary-100: #3B4C62;
|
||||
--c--theme--colors--primary-200: #4D6481;
|
||||
--c--theme--colors--primary-300: #6381A6;
|
||||
--c--theme--colors--primary-400: #7FA5D5;
|
||||
--c--theme--colors--primary-500: #8CB5EA;
|
||||
--c--theme--colors--primary-600: #A3C4EE;
|
||||
--c--theme--colors--primary-700: #C3D8F4;
|
||||
--c--theme--colors--primary-800: #DDE9F8;
|
||||
--c--theme--colors--primary-900: #F4F8FD;
|
||||
--c--theme--colors--success-100: #EEF8D7;
|
||||
--c--theme--colors--success-200: #D9F1B2;
|
||||
--c--theme--colors--success-300: #BDE985;
|
||||
--c--theme--colors--success-400: #A0E25D;
|
||||
--c--theme--colors--success-500: #76D628;
|
||||
--c--theme--colors--success-600: #5BB520;
|
||||
--c--theme--colors--success-700: #43941A;
|
||||
--c--theme--colors--success-800: #307414;
|
||||
--c--theme--colors--success-900: #225D10;
|
||||
--c--theme--colors--warning-100: #F7F3D5;
|
||||
--c--theme--colors--warning-200: #F0E5AA;
|
||||
--c--theme--colors--warning-300: #E8D680;
|
||||
--c--theme--colors--warning-400: #E3C95F;
|
||||
--c--theme--colors--warning-500: #D9B32B;
|
||||
--c--theme--colors--warning-600: #BD9721;
|
||||
--c--theme--colors--warning-700: #9D7B1C;
|
||||
--c--theme--colors--warning-800: #7E6016;
|
||||
--c--theme--colors--warning-900: #684D12;
|
||||
--c--theme--colors--danger-100: #F8D0D0;
|
||||
--c--theme--colors--danger-200: #F09898;
|
||||
--c--theme--colors--danger-300: #F09898;
|
||||
--c--theme--colors--danger-400: #ED8585;
|
||||
--c--theme--colors--danger-500: #E96666;
|
||||
--c--theme--colors--danger-600: #DD6666;
|
||||
--c--theme--colors--danger-700: #C36666;
|
||||
--c--theme--colors--danger-800: #AE6666;
|
||||
--c--theme--colors--danger-900: #9D6666;
|
||||
}
|
||||
30
src/frontend/src/index.scss
Normal file
30
src/frontend/src/index.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@use "@openfun/cunningham-react/sass/fonts";
|
||||
@use "@openfun/cunningham-react/sass/icons";
|
||||
@use "@openfun/cunningham-react/style";
|
||||
@use "cunningham-tokens";
|
||||
|
||||
:root {
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
background-color: var(--c--theme--colors--greyscale-000);
|
||||
}
|
||||
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
border-radius: 1rem;
|
||||
|
||||
h1, h3 {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
13
src/frontend/src/main.tsx
Normal file
13
src/frontend/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.scss'
|
||||
import {AppProvider} from "./components";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<AppProvider>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
7
src/frontend/src/vite-env.d.ts
vendored
Normal file
7
src/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_PUBLIC_API_ROOT_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
25
src/frontend/tsconfig.json
Normal file
25
src/frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
src/frontend/tsconfig.node.json
Normal file
10
src/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
6
src/frontend/vite.config.ts
Normal file
6
src/frontend/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user