Add feedback page
This commit is contained in:
parent
10dad92a5b
commit
4e0038d899
5 changed files with 192 additions and 3 deletions
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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
22
ui/src/hooks/feedback.ts
Normal 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
157
ui/src/pages/feedback.tsx
Normal 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 été 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue