From 0b9f68549cc941b694603aa7a77c1ac734a2a22f Mon Sep 17 00:00:00 2001
From: Christophe <christophe.misc+git@protonmail.ch>
Date: Sun, 17 Sep 2023 13:05:22 +0200
Subject: [PATCH] feat(template): allow rating template

---
 frontend/components/Rating.tsx             | 47 ++++++++++++++++++++++
 frontend/components/templates/Template.tsx | 37 +----------------
 frontend/pages/templates/[id].tsx          | 17 +++++++-
 3 files changed, 64 insertions(+), 37 deletions(-)
 create mode 100644 frontend/components/Rating.tsx

diff --git a/frontend/components/Rating.tsx b/frontend/components/Rating.tsx
new file mode 100644
index 0000000..66133a9
--- /dev/null
+++ b/frontend/components/Rating.tsx
@@ -0,0 +1,47 @@
+import { FC, useState } from 'react';
+import clsx from 'clsx';
+import styles from 'components/templates/Template.module.scss';
+import { StarHalf } from 'lucide-react';
+
+type RatingProps = { score: number; className?: string; onChange?: (score: number) => void };
+export const Rating: FC<RatingProps> = ({ score, className, onChange }) => {
+    const doubleScore = score * 2;
+
+    const [hoverScore, setHoverScore] = useState(doubleScore);
+    const [hovering, setHovering] = useState(false);
+
+    return (
+        <span
+            className={clsx(className)}
+            onMouseEnter={onChange ? () => setHovering(true) : undefined}
+            onMouseLeave={onChange ? () => setHovering(false) : undefined}
+        >
+            {[...Array(10)].map((_, i) => {
+                const classes = clsx(
+                    'inline',
+                    styles['star'],
+                    i % 2 == 1 ? styles['flipped'] : false
+                );
+                return (
+                    <span
+                        key={i}
+                        className={clsx(
+                            'text-yellow-500',
+                            'text-xl',
+                            'inline',
+                            'align-text-bottom'
+                        )}
+                        onMouseEnter={() => setHoverScore(i + (i % 2))}
+                        onClick={() => onChange && onChange((i + (i % 2)) / 2)}
+                    >
+                        {i < (hovering ? hoverScore : doubleScore) ? (
+                            <StarHalf fill="currentColor" className={classes} />
+                        ) : (
+                            <StarHalf className={classes} />
+                        )}
+                    </span>
+                );
+            })}
+        </span>
+    );
+};
diff --git a/frontend/components/templates/Template.tsx b/frontend/components/templates/Template.tsx
index ecf8717..751946e 100644
--- a/frontend/components/templates/Template.tsx
+++ b/frontend/components/templates/Template.tsx
@@ -4,42 +4,7 @@ import clsx from 'clsx';
 import { Template as TemplateDto } from 'lib/client/models/template';
 import Link from 'next/link';
 import Badge from 'components/Badge';
-import { StarHalf } from 'lucide-react';
-
-type RatingProps = { score: number; className?: string };
-const Rating: FC<RatingProps> = ({ score, className }) => {
-    const doubleScore = score * 2;
-
-    return (
-        <span className={clsx(className)}>
-            {[...Array(10)].map((_, i) => (
-                <span
-                    key={i}
-                    className={clsx('text-yellow-500', 'text-xl', 'inline', 'align-text-bottom')}
-                >
-                    {i < doubleScore ? (
-                        <StarHalf
-                            fill="currentColor"
-                            className={clsx(
-                                'inline',
-                                styles['star'],
-                                i % 2 == 1 ? styles['flipped'] : false
-                            )}
-                        />
-                    ) : (
-                        <StarHalf
-                            className={clsx(
-                                'inline',
-                                styles['star'],
-                                i % 2 == 1 ? styles['flipped'] : false
-                            )}
-                        />
-                    )}
-                </span>
-            ))}
-        </span>
-    );
-};
+import { Rating } from 'components/Rating';
 
 type TemplateProps = {
     template: TemplateDto;
diff --git a/frontend/pages/templates/[id].tsx b/frontend/pages/templates/[id].tsx
index 4e83305..35e13dc 100644
--- a/frontend/pages/templates/[id].tsx
+++ b/frontend/pages/templates/[id].tsx
@@ -1,7 +1,7 @@
 import { NextPage } from 'next';
 import Layout from 'components/Layout';
 import { useTemplateApi } from 'lib/useApi';
-import { useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
 import { useRouter } from 'next/router';
 import LoadingSpinner from 'components/LoadingSpinner';
 import { useEffect } from 'react';
@@ -11,6 +11,7 @@ import { firstMatching } from 'lib/firstMatching';
 import Badge from 'components/Badge';
 import { CutterField } from 'lib/client';
 import TemplateForm from 'pages/components/TemplateForm';
+import { Rating } from 'components/Rating';
 
 export const hasDefaultValue = (field: CutterField) => {
     // TODO: type assertion because of api spec/generator issue
@@ -29,8 +30,17 @@ const Template: NextPage = () => {
     const api = useTemplateApi();
     const template = useQuery(['template', templateId], () => api.getTemplate(templateId ?? ''), {
         enabled: templateId !== undefined,
+        keepPreviousData: true,
     });
 
+    const rateTemplate = useMutation(
+        ['rate', templateId],
+        (score: number) => api.rateTemplate(templateId ?? '', score),
+        {
+            onSuccess: () => template.refetch(),
+        }
+    );
+
     useEffect(() => {
         if (router.isReady && templateId === undefined) {
             router.push('/templates');
@@ -61,6 +71,11 @@ const Template: NextPage = () => {
             <div className="flex flex-row w-100">
                 <div className="flex-grow">
                     <h1 className="inline">{template.data.data.title}</h1>
+                    <Rating
+                        score={template.data.data.score ?? 0}
+                        onChange={(score) => rateTemplate.mutate(score)}
+                        className="ml-2"
+                    />
                     <div className="ml-2 inline-flex gap-1 align-text-top">
                         {Array.from(template.data.data.tags).map((tag) => (
                             <Badge type="info" key={tag}>
-- 
GitLab