Merge pull request #27 from ForFarmTeam/feature-profile-settings

feat: add setting and profile setting page
This commit is contained in:
Sirin Puenggun 2025-04-04 17:13:52 +07:00 committed by GitHub
commit 3dc5f37436
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 646 additions and 167 deletions

View File

@ -11,6 +11,8 @@ import (
"github.com/forfarm/backend/internal/domain"
"github.com/forfarm/backend/internal/utilities"
"github.com/go-chi/chi/v5"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/jackc/pgx/v5"
)
func (a *api) registerUserRoutes(_ chi.Router, api huma.API) {
@ -29,13 +31,25 @@ type getSelfDataInput struct {
Authorization string `header:"Authorization" required:"true" example:"Bearer token"`
}
// getSelfDataOutput uses domain.User which now has camelCase tags
type getSelfDataOutput struct {
Body struct {
User domain.User `json:"user"`
}
}
type UpdateSelfDataInput struct {
Authorization string `header:"Authorization" required:"true" example:"Bearer token"`
Body struct {
Username *string `json:"username,omitempty"`
}
}
type UpdateSelfDataOutput struct {
Body struct {
User domain.User `json:"user"`
}
}
func (a *api) getSelfData(ctx context.Context, input *getSelfDataInput) (*getSelfDataOutput, error) {
resp := &getSelfDataOutput{}
@ -71,3 +85,70 @@ func (a *api) getSelfData(ctx context.Context, input *getSelfDataInput) (*getSel
resp.Body.User = user
return resp, nil
}
func (a *api) updateSelfData(ctx context.Context, input *UpdateSelfDataInput) (*UpdateSelfDataOutput, error) {
userID, err := a.getUserIDFromHeader(input.Authorization)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
user, err := a.userRepo.GetByUUID(ctx, userID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, pgx.ErrNoRows) {
a.logger.Warn("Attempt to update non-existent user", "user_uuid", userID)
return nil, huma.Error404NotFound("User not found")
}
a.logger.Error("Failed to get user for update", "user_uuid", userID, "error", err)
return nil, huma.Error500InternalServerError("Failed to retrieve user for update")
}
updated := false
if input.Body.Username != nil {
trimmedUsername := strings.TrimSpace(*input.Body.Username)
if trimmedUsername != user.Username {
err := validation.Validate(trimmedUsername,
validation.Required.Error("username cannot be empty if provided"),
validation.Length(3, 30).Error("username must be between 3 and 30 characters"),
)
if err != nil {
return nil, huma.Error422UnprocessableEntity("Invalid username", err)
}
user.Username = trimmedUsername
updated = true
}
}
// Check other field here la
if !updated {
a.logger.Info("No changes detected for user update", "user_uuid", userID)
return &UpdateSelfDataOutput{Body: struct {
User domain.User `json:"user"`
}{User: user}}, nil
}
// Validate the *entire* user object after updates (optional but good practice)
// if err := user.Validate(); err != nil {
// return nil, huma.Error422UnprocessableEntity("Validation failed after update", err)
// }
// Save updated user
err = a.userRepo.CreateOrUpdate(ctx, &user)
if err != nil {
a.logger.Error("Failed to update user in database", "user_uuid", userID, "error", err)
return nil, huma.Error500InternalServerError("Failed to save user profile")
}
a.logger.Info("User profile updated successfully", "user_uuid", userID)
updatedUser, fetchErr := a.userRepo.GetByUUID(ctx, userID)
if fetchErr != nil {
a.logger.Error("Failed to fetch user data after update", "user_uuid", userID, "error", fetchErr)
return &UpdateSelfDataOutput{Body: struct {
User domain.User `json:"user"`
}{User: user}}, nil
}
return &UpdateSelfDataOutput{Body: struct {
User domain.User `json:"user"`
}{User: updatedUser}}, nil
}

29
frontend/api/profile.ts Normal file
View File

@ -0,0 +1,29 @@
import axiosInstance from "./config";
import type { User } from "@/types";
export interface UpdateUserProfileInput {
username?: string;
// email?: string;
// avatar?: string;
}
/**
* Updates the current user's profile information.
* Sends a PUT request to the /user/me endpoint.
* @param data - An object containing the fields to update.
* @returns The updated user data from the backend.
*/
export async function updateUserProfile(data: UpdateUserProfileInput): Promise<User> {
try {
// Backend expects { user: ... } in the response body
const response = await axiosInstance.put<{ user: User }>("/user/me", data);
return response.data.user;
} catch (error) {
console.error("Error updating user profile:", error);
throw new Error(
(error as any).response?.data?.detail ||
(error as any).response?.data?.message ||
"Failed to update profile. Please try again."
);
}
}

View File

@ -0,0 +1,269 @@
// frontend/app/(sidebar)/profile/page.tsx
"use client";
import React, { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { toast } from "sonner";
import { Loader2, User as UserIcon, Mail, Save, X, Edit, Camera } from "lucide-react";
import { fetchUserMe, UserDataOutput } from "@/api/user"; // Fetch function
import { updateUserProfile, UpdateUserProfileInput } from "@/api/profile"; // Update function
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton"; // For loading state
// Schema for editable fields
const profileSchema = z.object({
username: z
.string()
.min(3, "Username must be at least 3 characters")
.max(30, "Username cannot exceed 30 characters")
.optional() // Make it optional if user doesn't have one initially
.or(z.literal("")), // Allow empty string
});
type ProfileFormData = z.infer<typeof profileSchema>;
export default function ProfilePage() {
const queryClient = useQueryClient();
const [isEditing, setIsEditing] = useState(false);
// Fetch current user data
const {
data: userData,
isLoading,
isError,
error,
} = useQuery<UserDataOutput>({
queryKey: ["userMe"],
queryFn: fetchUserMe,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
const user = userData?.user;
// Setup react-hook-form
const form = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: {
username: "",
},
});
// Populate form when user data loads or edit mode changes
useEffect(() => {
if (user) {
form.reset({
username: user.username || "",
});
}
}, [user, isEditing, form.reset]);
// Mutation for updating profile
const mutation = useMutation({
mutationFn: updateUserProfile,
onSuccess: (updatedUser) => {
toast.success("Profile updated successfully!");
// Update the cache with the new user data
queryClient.setQueryData(["userMe"], { user: updatedUser });
setIsEditing(false); // Exit edit mode
},
onError: (error) => {
toast.error(`Failed to update profile: ${error.message}`);
},
});
const handleEditToggle = () => {
if (isEditing) {
// Reset form to original values if canceling edit
form.reset({ username: user?.username || "" });
}
setIsEditing(!isEditing);
};
const onSubmit = (formData: ProfileFormData) => {
const updatePayload: UpdateUserProfileInput = {};
// Only include username if it actually changed
if (formData.username !== undefined && formData.username !== (user?.username || "")) {
// Allow setting to empty string if desired, or add validation to prevent it
updatePayload.username = formData.username;
}
if (Object.keys(updatePayload).length > 0) {
mutation.mutate(updatePayload);
} else {
// No changes were made
setIsEditing(false); // Just exit edit mode
}
};
// --- Render Logic ---
if (isLoading) {
return (
<div className="p-6 space-y-4">
<Skeleton className="h-8 w-1/4 mb-4" />
<Card>
<CardHeader>
<Skeleton className="h-6 w-1/3" />
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-4">
<Skeleton className="h-20 w-20 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-64" />
</div>
</div>
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</CardContent>
</Card>
</div>
);
}
if (isError) {
return <div className="p-6 text-destructive">Error loading profile: {(error as Error)?.message}</div>;
}
if (!user) {
return <div className="p-6 text-muted-foreground">User data not found.</div>;
}
return (
<div className="p-4 md:p-6 space-y-6">
<h1 className="text-2xl font-bold">User Profile</h1>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Account Information</CardTitle>
<CardDescription>View and manage your personal details.</CardDescription>
</div>
<Button
variant={isEditing ? "outline" : "default"}
size="sm"
onClick={handleEditToggle}
disabled={mutation.isPending}>
{isEditing ? <X className="mr-2 h-4 w-4" /> : <Edit className="mr-2 h-4 w-4" />}
{isEditing ? "Cancel" : "Edit Profile"}
</Button>
</div>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="flex flex-col sm:flex-row items-center gap-6">
{/* Avatar Section (Placeholder for upload) */}
<div className="flex flex-col items-center gap-2">
<Avatar className="h-24 w-24 border-2 border-primary/20">
<AvatarImage
src={user.avatar || `https://api.dicebear.com/9.x/initials/svg?seed=${user.email}`}
alt={user.username || user.email}
/>
<AvatarFallback className="text-lg">
{user.username ? user.username.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<Button
variant="outline"
size="sm"
disabled={isEditing || mutation.isPending}
onClick={() => toast.info("Avatar upload coming soon!")}>
<Camera className="mr-2 h-3 w-3" /> Change
</Button>
</div>
<div className="flex-1 space-y-4 w-full">
{/* Username Field */}
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<Label htmlFor="username" className="flex items-center gap-1">
<UserIcon className="h-4 w-4 text-muted-foreground" /> Username
</Label>
<FormControl>
<Input
id="username"
placeholder="Enter your username"
{...field}
readOnly={!isEditing}
className={
!isEditing
? "border-none bg-transparent px-1 shadow-none read-only:focus-visible:ring-0 read-only:focus-visible:ring-offset-0"
: ""
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email Field (Read-only) */}
<div className="space-y-1">
<Label htmlFor="email" className="flex items-center gap-1">
<Mail className="h-4 w-4 text-muted-foreground" /> Email
</Label>
<Input
id="email"
type="email"
value={user.email}
readOnly
className="border-none bg-transparent px-1 shadow-none read-only:focus-visible:ring-0 read-only:focus-visible:ring-offset-0"
/>
<p className="text-xs text-muted-foreground px-1">Email cannot be changed currently.</p>
</div>
{/* User ID (Read-only) */}
<div className="space-y-1">
<Label htmlFor="userId" className="flex items-center gap-1">
ID
</Label>
<Input
id="userId"
value={user.uuid}
readOnly
className="border-none bg-transparent px-1 shadow-none text-xs text-muted-foreground read-only:focus-visible:ring-0 read-only:focus-visible:ring-offset-0"
/>
</div>
</div>
</div>
{/* Save Button (Visible only in edit mode) */}
{isEditing && (
<>
<Separator />
<div className="flex justify-end">
<Button type="submit" disabled={mutation.isPending || !form.formState.isDirty}>
{mutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Saving...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" /> Save Changes
</>
)}
</Button>
</div>
</>
)}
</form>
</Form>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,94 @@
"use client";
import React from "react";
import { useTheme } from "next-themes";
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Paintbrush, User, Trash2, ExternalLink } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
export default function SettingsPage() {
const { theme, setTheme } = useTheme();
const handleDeleteAccount = () => {
toast.warning("Account deletion is not yet implemented.", {
description: "This feature will be available in a future update.",
action: { label: "Close", onClick: () => toast.dismiss() },
});
};
return (
<div className="p-4 md:p-6 space-y-8">
<h1 className="text-2xl font-bold">Settings</h1>
{/* Appearance Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Paintbrush className="h-5 w-5 text-primary" /> Appearance
</CardTitle>
<CardDescription>Customize the look and feel of the application.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<Label htmlFor="theme" className="whitespace-nowrap">
Theme
</Label>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger id="theme" className="w-full sm:w-[180px]">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
{/* Add other appearance settings here if needed */}
</CardContent>
</Card>
{/* Account Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" /> Account
</CardTitle>
<CardDescription>Manage your account details and security.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/profile" passHref>
<Button variant="outline" className="w-full justify-between">
<span>Edit Profile Information</span>
<ExternalLink className="h-4 w-4 text-muted-foreground" />
</Button>
</Link>
<Button
variant="outline"
className="w-full justify-between"
onClick={() => toast.info("Password change coming soon!")}>
<span>Change Password</span>
<ExternalLink className="h-4 w-4 text-muted-foreground" />
</Button>
</CardContent>
<Separator className="my-4" />
<CardFooter className="flex flex-col items-start gap-4">
<div>
<h4 className="font-medium text-destructive">Danger Zone</h4>
<p className="text-sm text-muted-foreground mt-1">
Permanently delete your account and all associated data. This action cannot be undone.
</p>
</div>
<Button variant="destructive" onClick={handleDeleteAccount} className="gap-2">
<Trash2 className="h-4 w-4" /> Delete Account
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@ -1,141 +1,147 @@
"use client";
import { useState } from "react";
import PlantingDetailsForm from "./planting-detail-form";
import HarvestDetailsForm from "./harvest-detail-form";
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
import { Separator } from "@/components/ui/separator";
import {
plantingDetailsFormSchema,
harvestDetailsFormSchema,
} from "@/schemas/application.schema";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
// "use client";
// import { useState } from "react";
// import PlantingDetailsForm from "./planting-detail-form";
// import HarvestDetailsForm from "./harvest-detail-form";
// import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
// import { Separator } from "@/components/ui/separator";
// import {
// plantingDetailsFormSchema,
// harvestDetailsFormSchema,
// } from "@/schemas/application.schema";
// import { z } from "zod";
// import { Button } from "@/components/ui/button";
// import { toast } from "sonner";
import { redirect } from "next/navigation";
type PlantingSchema = z.infer<typeof plantingDetailsFormSchema>;
type HarvestSchema = z.infer<typeof harvestDetailsFormSchema>;
// type PlantingSchema = z.infer<typeof plantingDetailsFormSchema>;
// type HarvestSchema = z.infer<typeof harvestDetailsFormSchema>;
const steps = [
{ title: "Step 1", description: "Planting Details" },
{ title: "Step 2", description: "Harvest Details" },
{ title: "Step 3", description: "Select Map Area" },
];
// const steps = [
// { title: "Step 1", description: "Planting Details" },
// { title: "Step 2", description: "Harvest Details" },
// { title: "Step 3", description: "Select Map Area" },
// ];
// export default function SetupPage() {
// const [step, setStep] = useState(1);
// const [plantingDetails, setPlantingDetails] = useState<PlantingSchema | null>(
// null
// );
// const [harvestDetails, setHarvestDetails] = useState<HarvestSchema | null>(
// null
// );
// const [mapData, setMapData] = useState<{ lat: number; lng: number }[] | null>(
// null
// );
// const handleNext = () => {
// if (step === 1 && !plantingDetails) {
// toast.warning(
// "Please complete the Planting Details before proceeding.",
// {
// action: {
// label: "Close",
// onClick: () => toast.dismiss(),
// },
// }
// );
// return;
// }
// if (step === 2 && !harvestDetails) {
// toast.warning(
// "Please complete the Harvest Details before proceeding.",
// {
// action: {
// label: "Close",
// onClick: () => toast.dismiss(),
// },
// }
// );
// return;
// }
// setStep((prev) => prev + 1);
// };
// const handleBack = () => {
// setStep((prev) => prev - 1);
// };
// const handleSubmit = () => {
// if (!mapData) {
// toast.warning("Please select an area on the map before submitting.", {
// action: {
// label: "Close",
// onClick: () => toast.dismiss(),
// },
// });
// return;
// }
// console.log("Submitting:", { plantingDetails, harvestDetails, mapData });
// // send request to the server
// };
// return (
// <div className="p-5">
// {/* Stepper Navigation */}
// <div className="flex justify-between items-center mb-5">
// {steps.map((item, index) => (
// <div key={index} className="flex flex-col items-center">
// <div
// className={`w-10 h-10 flex items-center justify-center rounded-full text-white font-bold ${
// step === index + 1 ? "bg-blue-500" : "bg-gray-500"
// }`}
// >
// {index + 1}
// </div>
// <span className="font-medium mt-2">{item.title}</span>
// <span className="text-gray-500 text-sm">{item.description}</span>
// </div>
// ))}
// </div>
// <Separator className="mb-5" />
// {step === 1 && (
// <>
// <h2 className="text-xl text-center mb-5">Planting Details</h2>
// <PlantingDetailsForm onChange={setPlantingDetails} />
// </>
// )}
// {step === 2 && (
// <>
// <h2 className="text-xl text-center mb-5">Harvest Details</h2>
// <HarvestDetailsForm onChange={setHarvestDetails} />
// </>
// )}
// {step === 3 && (
// <>
// <h2 className="text-xl text-center mb-5">Select Area on Map</h2>
// <GoogleMapWithDrawing onAreaSelected={setMapData} />
// </>
// )}
// <div className="mt-10 flex justify-between">
// <Button onClick={handleBack} disabled={step === 1}>
// Back
// </Button>
// {step < 3 ? (
// <Button onClick={handleNext}>Next</Button>
// ) : (
// <Button onClick={handleSubmit}>Submit</Button>
// )}
// </div>
// </div>
// );
// }
export default function SetupPage() {
const [step, setStep] = useState(1);
const [plantingDetails, setPlantingDetails] = useState<PlantingSchema | null>(
null
);
const [harvestDetails, setHarvestDetails] = useState<HarvestSchema | null>(
null
);
const [mapData, setMapData] = useState<{ lat: number; lng: number }[] | null>(
null
);
const handleNext = () => {
if (step === 1 && !plantingDetails) {
toast.warning(
"Please complete the Planting Details before proceeding.",
{
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
}
);
return;
}
if (step === 2 && !harvestDetails) {
toast.warning(
"Please complete the Harvest Details before proceeding.",
{
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
}
);
return;
}
setStep((prev) => prev + 1);
};
const handleBack = () => {
setStep((prev) => prev - 1);
};
const handleSubmit = () => {
if (!mapData) {
toast.warning("Please select an area on the map before submitting.", {
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
});
return;
}
console.log("Submitting:", { plantingDetails, harvestDetails, mapData });
// send request to the server
};
return (
<div className="p-5">
{/* Stepper Navigation */}
<div className="flex justify-between items-center mb-5">
{steps.map((item, index) => (
<div key={index} className="flex flex-col items-center">
<div
className={`w-10 h-10 flex items-center justify-center rounded-full text-white font-bold ${
step === index + 1 ? "bg-blue-500" : "bg-gray-500"
}`}
>
{index + 1}
</div>
<span className="font-medium mt-2">{item.title}</span>
<span className="text-gray-500 text-sm">{item.description}</span>
</div>
))}
</div>
<Separator className="mb-5" />
{step === 1 && (
<>
<h2 className="text-xl text-center mb-5">Planting Details</h2>
<PlantingDetailsForm onChange={setPlantingDetails} />
</>
)}
{step === 2 && (
<>
<h2 className="text-xl text-center mb-5">Harvest Details</h2>
<HarvestDetailsForm onChange={setHarvestDetails} />
</>
)}
{step === 3 && (
<>
<h2 className="text-xl text-center mb-5">Select Area on Map</h2>
<GoogleMapWithDrawing onAreaSelected={setMapData} />
</>
)}
<div className="mt-10 flex justify-between">
<Button onClick={handleBack} disabled={step === 1}>
Back
</Button>
{step < 3 ? (
<Button onClick={handleNext}>Next</Button>
) : (
<Button onClick={handleSubmit}>Submit</Button>
)}
</div>
</div>
);
// redirect to /farms
redirect("/farms");
}

View File

@ -10,21 +10,21 @@ import {
GalleryVerticalEnd,
Map,
PieChart,
Settings2,
Settings,
SquareTerminal,
User,
UserCircle,
} from "lucide-react";
import { NavMain } from "./nav-main";
import { NavUser } from "./nav-user";
import { TeamSwitcher } from "./team-switcher";
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from "@/components/ui/sidebar";
import { NavCrops } from "./nav-crops";
import { Sidebar, SidebarContent, SidebarHeader, SidebarRail } from "@/components/ui/sidebar";
// import { NavCrops } from "./nav-crops";
import { fetchUserMe } from "@/api/user";
import { usePathname } from "next/navigation";
interface Team {
name: string;
logo: React.ComponentType;
logo: React.ComponentType<{ className?: string }>; // Ensure logo type accepts className
plan: string;
}
@ -34,12 +34,13 @@ interface NavItem {
title: string;
url: string;
icon: LucideIcon;
isActive?: boolean; // Add isActive property
}
interface SidebarConfig {
teams: Team[];
navMain: NavItem[];
crops: NavItem[];
// crops: NavItem[];
}
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
@ -69,27 +70,28 @@ function UserErrorFallback({ message }: { message: string }) {
);
}
const defaultNavMain: NavItem[] = [
{ title: "Farms", url: "/farms", icon: Map },
{ title: "Inventory", url: "/inventory", icon: SquareTerminal },
{ title: "Marketplace", url: "/marketplace", icon: PieChart }, // Updated title and icon
{ title: "Knowledge Hub", url: "/hub", icon: BookOpen },
{ title: "AI Chatbot", url: "/chatbot", icon: Bot },
{ title: "Profile", url: "/profile", icon: UserCircle }, // Added Profile
{ title: "Settings", url: "/settings", icon: Settings }, // Kept Settings
];
export function AppSidebar({ config, ...props }: AppSidebarProps) {
const pathname = usePathname();
const defaultConfig: SidebarConfig = {
teams: [
{ name: "Farm 1", logo: GalleryVerticalEnd, plan: "Hatyai" },
{ name: "Farm 2", logo: AudioWaveform, plan: "Songkla" },
{ name: "Farm 3", logo: Command, plan: "Layong" },
],
navMain: [
{ title: "Farms", url: "/farms", icon: Map },
{ title: "Inventory", url: "/inventory", icon: SquareTerminal },
{ title: "Marketplace Information", url: "/marketplace", icon: PieChart },
{ title: "Knowledge Hub", url: "/hub", icon: BookOpen },
{ title: "Users", url: "/users", icon: User },
{ title: "AI Chatbot", url: "/chatbot", icon: Bot },
{ title: "Settings", url: "/settings", icon: Settings2 },
],
crops: [
{ title: "Crops 1", url: "/farms/[farmId]/crops/1", icon: Map },
{ title: "Crops 2", url: "/farms/[farmId]/crops/2", icon: Map },
{ title: "Crops 3", url: "/farms/[farmId]/crops/3", icon: Map },
],
navMain: defaultNavMain.map((item) => ({
...item,
isActive: pathname.startsWith(item.url) && (item.url !== "/" || pathname === "/"),
})),
};
// Allow external configuration override
@ -107,17 +109,18 @@ export function AppSidebar({ config, ...props }: AppSidebarProps) {
async function getUser() {
try {
const data = await fetchUserMe();
console.log(data);
console.log("Fetched user data:", data);
setUser({
name: data.user.uuid,
name: data.user.username || data.user.email.split("@")[0] || `User ${data.user.uuid.substring(0, 6)}`,
email: data.user.email,
avatar: data.user.avatar || "/avatars/avatar.webp",
avatar: data.user.avatar || `https://api.dicebear.com/9.x/initials/svg?seed=${data.user.email}`,
});
} catch (err: unknown) {
console.error("Failed to fetch user for sidebar:", err);
if (err instanceof Error) {
setError(err.message);
} else {
setError("An unexpected error occurred");
setError("An unexpected error occurred fetching user data");
}
} finally {
setLoading(false);
@ -129,17 +132,14 @@ export function AppSidebar({ config, ...props }: AppSidebarProps) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={sidebarConfig.teams} />
{loading ? <UserSkeleton /> : error ? <UserErrorFallback message={error} /> : <NavUser user={user} />}
</SidebarHeader>
<SidebarContent>
<NavMain items={sidebarConfig.navMain} />
<div className="mt-6">
<NavCrops crops={sidebarConfig.crops} />
</div>
</SidebarContent>
<SidebarFooter>
{/* <SidebarFooter>
{loading ? <UserSkeleton /> : error ? <UserErrorFallback message={error} /> : <NavUser user={user} />}
</SidebarFooter>
</SidebarFooter> */}
<SidebarRail />
</Sidebar>
);