Add feedback page

This commit is contained in:
Arthur Belleville 2025-06-28 14:52:57 +02:00
parent 10dad92a5b
commit 4e0038d899
No known key found for this signature in database
5 changed files with 192 additions and 3 deletions

View file

@ -16,6 +16,7 @@ import { FacturesPage } from "./pages/factures";
import { PlanningPage } from "./pages/planning";
import { ChantiersPage } from "./pages/chantiers";
import { ChatPage } from "./pages/chat";
import { FeedbackPage } from "./pages/feedback";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
import ChatProvider from "./providers/ChatProvider";
import { UserStoreProvider } from "./providers/UserStoreProvider";
@ -83,6 +84,14 @@ export const App = () => {
</Layout>
}
/>
<Route
path="feedback"
element={
<Layout>
<FeedbackPage />
</Layout>
}
/>
</Route>
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
<Route path="landing" element={<LandingPage />} />

View file

@ -354,7 +354,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
<li>
<NavLink>
<RouterLink
to="/"
to="/feedback"
className={twMerge("w-full", isCollapsed ? "" : "pl-2")}
aria-label={isCollapsed ? "Feedback" : undefined}
>

22
ui/src/hooks/feedback.ts Normal file
View file

@ -0,0 +1,22 @@
import { useMutation } from "@tanstack/react-query";
import { supabase } from "./auth";
import { useUser } from "@ui/providers/UserStoreProvider";
import { FeedbackData } from "@ui/pages/feedback";
// Create new feedback
export const useCreateFeedback = () => {
const user = useUser();
const { mutate, isSuccess, isPending } = useMutation({
mutationFn: async ({ fd_type, message }: FeedbackData) => {
const { error } = await supabase.from("feedbacks").insert({
fd_type,
message,
user_id: user?.id ?? "",
});
if (error) throw error;
},
onSuccess: () => {},
});
return { createFeedback: mutate, isSuccess, isPending };
};

157
ui/src/pages/feedback.tsx Normal file
View file

@ -0,0 +1,157 @@
import React, { useState } from "react";
import { Button } from "@ui/ui-library/button";
import { Form } from "@ui/ui-library/form";
import { TextField, Label, TextArea, Description } from "@ui/ui-library/field";
import { Text } from "@ui/ui-library/text";
import { Separator } from "react-aria-components";
import { SendIcon, ArrowLeftIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { useNavigate } from "react-router-dom";
import { useCreateFeedback } from "@ui/hooks/feedback";
export interface FeedbackData {
fd_type: "bug" | "feature" | "improvement" | "other";
message: string;
}
export function FeedbackPage() {
const navigate = useNavigate();
const [formData, setFormData] = useState<FeedbackData>({
fd_type: "improvement",
message: "",
});
const { createFeedback, isSuccess, isPending } = useCreateFeedback();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
createFeedback(formData);
};
const handleInputChange = (field: keyof FeedbackData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<div className="max-w-2xl mx-auto p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-4 mb-4">
<Button
variant="outline"
isIconOnly
onPress={() => navigate(-1)}
aria-label="Retour"
className="shrink-0"
>
<ArrowLeftIcon className="w-4 h-4" />
</Button>
<div>
<Text className="text-2xl font-bold">Envoyer un commentaire</Text>
<Text className="text-gray-600 dark:text-gray-400 mt-1">
Aidez-nous à améliorer XTablo en partageant vos idées
</Text>
</div>
</div>
</div>
<Separator className="mb-6" />
{isSuccess ? (
<div className="text-center py-12">
<div className="text-green-600 mb-4">
<SendIcon className="w-12 h-12 mx-auto" />
</div>
<Text className="text-xl font-medium text-green-600 mb-2">
Merci pour votre commentaire !
</Text>
<Text className="text-gray-600 dark:text-gray-400 mb-6">
Votre commentaire a é envoyé avec succès. Nous apprécions que vous
ayez pris le temps de nous aider à nous améliorer.
</Text>
<Button variant="outline" onPress={() => navigate(-1)}>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Retour
</Button>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg border p-6">
<Form onSubmit={handleSubmit} className="space-y-6">
{/* Feedback Type */}
<TextField>
<Label>Type de commentaire</Label>
<select
value={formData.fd_type}
onChange={(e) =>
handleInputChange(
"fd_type",
e.target.value as FeedbackData["fd_type"]
)
}
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
>
<option value="bug">Signaler un bug</option>
<option value="feature">Demande de fonctionnalité</option>
<option value="improvement">Amélioration</option>
<option value="other">Autre</option>
</select>
</TextField>
{/* Message Field */}
<TextField>
<Label>Message</Label>
<TextArea
value={formData.message}
onChange={(e) => handleInputChange("message", e.target.value)}
placeholder="Veuillez décrire votre commentaire en détail..."
rows={6}
required
className="resize-none"
/>
<Description>
Soyez aussi précis que possible pour nous aider à mieux
comprendre votre commentaire.
</Description>
</TextField>
{/* Submit Button */}
<div className="flex justify-end gap-3 pt-4">
<Button
variant="outline"
onPress={() => navigate(-1)}
type="button"
>
Annuler
</Button>
<Button
variant="solid"
type="submit"
isDisabled={isPending || !formData.message}
className="min-w-32"
>
{isPending ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Envoi en cours...
</span>
) : (
<span className="flex items-center gap-2">
<SendIcon className="w-4 h-4" />
Envoyer le commentaire
</span>
)}
</Button>
</div>
</Form>
</div>
)}
</div>
);
}

View file

@ -19,6 +19,7 @@ export const UserStoreProvider = ({
children: React.ReactNode;
}) => {
const { session } = useSession();
const shouldFetchUser = !!session?.access_token;
const { data, isPending } = useQuery<User | null>({
queryKey: ["user"],
queryFn: async () => {
@ -42,10 +43,10 @@ export const UserStoreProvider = ({
streamToken: token,
};
},
enabled: !!session?.access_token,
enabled: shouldFetchUser,
});
if (isPending) {
if (isPending && shouldFetchUser) {
return <LoadingSpinner />;
}