Add UI stuff for editing feed
This commit is contained in:
83
src/components/FeedDialog.tsx
Normal file
83
src/components/FeedDialog.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Checkbox } from "./ui/checkbox";
|
||||
import { Button } from "./ui/button";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export interface FeedDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (name: string, isPublic: boolean) => void;
|
||||
initialName?: string;
|
||||
initialIsPublic?: boolean;
|
||||
mode?: "create" | "edit";
|
||||
}
|
||||
|
||||
export function FeedDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialName = "",
|
||||
initialIsPublic = false,
|
||||
mode = "create",
|
||||
}: FeedDialogProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [isPublic, setIsPublic] = useState(initialIsPublic);
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
setIsPublic(initialIsPublic);
|
||||
}, [initialName, initialIsPublic]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (name.trim()) {
|
||||
onSubmit(name.trim(), isPublic);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "edit" ? "Edit Feed" : "Create New Feed"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="feed-name">Feed Name</Label>
|
||||
<Input
|
||||
id="feed-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter feed name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="is-public"
|
||||
checked={isPublic}
|
||||
onCheckedChange={(val) => setIsPublic(Boolean(val))}
|
||||
/>
|
||||
<Label htmlFor="is-public">Make feed public</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleSubmit}>
|
||||
{mode === "edit" ? "Save Changes" : "Create Feed"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
15
src/components/ui/badge.tsx
Normal file
15
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// components/ui/badge.tsx
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Badge({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded font-semibold inline-block",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
23
src/components/ui/checkbox.tsx
Normal file
23
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
@@ -91,11 +91,11 @@ export const addNewFeed = async (name: string, isPublic: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Authentication required');
|
||||
throw new Error("Authentication required");
|
||||
}
|
||||
|
||||
const response = await userDataApi.post(
|
||||
'/api/addnewfeed',
|
||||
"/api/addnewfeed",
|
||||
{
|
||||
Name: name,
|
||||
IsPublic: isPublic,
|
||||
@@ -108,10 +108,37 @@ export const addNewFeed = async (name: string, isPublic: boolean) => {
|
||||
);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
throw error.response?.data || { success: false, message: 'Network error' };
|
||||
throw error.response?.data || { success: false, message: "Network error" };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFeed = async (feedId: string, name: string, isPublic: boolean): Promise<boolean> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error("Authentication required");
|
||||
}
|
||||
|
||||
const response = await userDataApi.patch(
|
||||
`/api/updatefeed`,
|
||||
{
|
||||
feedId: feedId,
|
||||
Name: name,
|
||||
IsPublic: isPublic,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.status === 200;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const deleteFeed = async (feedId: string) => {
|
||||
try {
|
||||
|
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { isAuthenticated } from "@/lib/auth";
|
||||
import { useFeedStore } from "@/lib/store";
|
||||
import { addNewFeed } from "@/lib/api";
|
||||
import { addNewFeed, updateFeed } from "@/lib/api";
|
||||
import { formatDateTime, getTimeRangeOptions } from "@/lib/timeRanges";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Plus, RefreshCw, ScanBarcode } from "lucide-react";
|
||||
import { Plus, RefreshCw, ScanBarcode, Edit, Copy } from "lucide-react";
|
||||
import { FeedChart } from "@/components/FeedChart";
|
||||
import { FeedActions } from "@/components/FeedActions";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { FeedDialog } from "@/components/FeedDialog";
|
||||
|
||||
export function DashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -36,7 +37,6 @@ export function DashboardPage() {
|
||||
deleteFeed,
|
||||
} = useFeedStore();
|
||||
|
||||
// Check if user is authenticated
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated()) {
|
||||
toast.error("Please login to access the dashboard");
|
||||
@@ -44,11 +44,9 @@ export function DashboardPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch feeds on component mount
|
||||
fetchFeeds();
|
||||
}, [navigate, fetchFeeds]);
|
||||
|
||||
// Handle time range change
|
||||
const handleTimeRangeChange = (value: string) => {
|
||||
setTimeRangeValue(value);
|
||||
const selectedOption = getTimeRangeOptions().find(option => option.value === value);
|
||||
@@ -58,36 +56,49 @@ export function DashboardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle add new feed
|
||||
const handleAddNewFeed = async () => {
|
||||
try {
|
||||
const defaultName = `Untitled Feed ${new Date().toLocaleString()}`;
|
||||
const response = await addNewFeed(defaultName);
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit">("create");
|
||||
const [editFeedData, setEditFeedData] = useState<{ id: string; name: string; isPublic: boolean } | null>(null);
|
||||
|
||||
const handleAddNewFeed = () => {
|
||||
setDialogMode("create");
|
||||
setEditFeedData(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDialogSubmit = async (name: string, isPublic: boolean) => {
|
||||
try {
|
||||
if (dialogMode === "create") {
|
||||
const response = await addNewFeed(name, isPublic);
|
||||
if (response.success) {
|
||||
toast.success("New feed created successfully");
|
||||
toast.success("Feed created successfully");
|
||||
fetchFeeds();
|
||||
} else {
|
||||
toast.error("Failed to create new feed");
|
||||
toast.error(response.message || "Failed to create feed");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while creating a new feed");
|
||||
console.error(error);
|
||||
} else if (dialogMode === "edit" && editFeedData) {
|
||||
const success = await updateFeed(editFeedData.id, name, isPublic);
|
||||
if (success) {
|
||||
toast.success("Feed updated successfully");
|
||||
fetchFeeds();
|
||||
} else {
|
||||
toast.error("Failed to update feed");
|
||||
fetchFeeds();
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "An error occurred");
|
||||
}
|
||||
};
|
||||
|
||||
// Handle refresh
|
||||
const handleRefresh = async () => {
|
||||
// First refresh the feeds list
|
||||
await fetchFeeds();
|
||||
// Then refresh current feed data if a feed is selected
|
||||
if (selectedFeed) {
|
||||
await refreshCurrentFeedData();
|
||||
}
|
||||
toast.success("Data refreshed");
|
||||
};
|
||||
|
||||
// Handle delete feed
|
||||
const handleDeleteFeed = async (feedId: string) => {
|
||||
try {
|
||||
await deleteFeed(feedId);
|
||||
@@ -108,7 +119,6 @@ export function DashboardPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Button to add a new feed */}
|
||||
<Button size="sm" onClick={handleAddNewFeed} disabled={isLoading}>
|
||||
Add Feed
|
||||
<Plus className="ml-2 h-4 w-4" />
|
||||
@@ -157,26 +167,32 @@ export function DashboardPage() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{feeds.map((feed) => (
|
||||
<div
|
||||
key={feed.id}
|
||||
className="flex items-center gap-1"
|
||||
<div key={feed.id} className="flex items-center justify-between border p-2 rounded">
|
||||
<div className="cursor-pointer text-sm font-medium flex items-center gap-2" onClick={() => selectFeed(feed)}>
|
||||
{feed.name}
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full font-medium ${feed.isPublic ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
variant={selectedFeed?.id === feed.id ? "default" : "outline"}
|
||||
className="w-full justify-start text-left flex-grow overflow-hidden"
|
||||
onClick={() => selectFeed(feed)}
|
||||
>
|
||||
<div className="flex flex-col items-start w-full overflow-hidden">
|
||||
<div className="font-medium truncate w-full">
|
||||
{feed.name || `Feed ${feed.id.substring(0, 8)}...`}
|
||||
{feed.isPublic ? 'Public' : 'Private'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground truncate w-full">
|
||||
Created: {formatDateTime(feed.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<FeedActions feed={feed} onDelete={handleDeleteFeed} />
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
onClick={() => {
|
||||
setDialogMode("edit");
|
||||
setEditFeedData({ id: feed.id, name: feed.name, isPublic: feed.isPublic });
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -206,9 +222,7 @@ export function DashboardPage() {
|
||||
toast.success("Feed ID copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16h8M8 12h8m-6 8h6a2 2 0 002-2v-5m0-4V6a2 2 0 00-2-2h-6l-2 2H6a2 2 0 00-2 2v12a2 2 0 002 2h2" />
|
||||
</svg>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</>
|
||||
@@ -219,7 +233,6 @@ export function DashboardPage() {
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
{/* Flex container for the select box and button */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={timeRangeValue}
|
||||
@@ -238,7 +251,6 @@ export function DashboardPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Button to refresh feed data */}
|
||||
<button
|
||||
className={`flex items-center p-1 border rounded-md
|
||||
${isLoading || !selectedFeed
|
||||
@@ -277,6 +289,14 @@ export function DashboardPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FeedDialog
|
||||
open={isDialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onSubmit={handleDialogSubmit}
|
||||
initialName={editFeedData?.name}
|
||||
initialIsPublic={editFeedData?.isPublic}
|
||||
mode={dialogMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Reference in New Issue
Block a user