Compare commits
5 Commits
117edf2cf1
...
8bd6a5ba84
Author | SHA1 | Date | |
---|---|---|---|
8bd6a5ba84 | |||
2b8f664e59 | |||
db32cbfdf2 | |||
d60607a424 | |||
8e36304b5a |
1029
pnpm-lock.yaml
generated
1029
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,9 @@ import { DashboardPage } from "@/pages/DashboardPage";
|
||||
import { ApiKeysPage } from "@/pages/ApiKeysPage";
|
||||
import { ProfilePage } from "@/pages/ProfilePage";
|
||||
import { VerifyEmail } from "@/pages/VerifyEmail";
|
||||
import { PublicFeedPage } from "@/pages/PublicFeedPage";
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -52,7 +55,9 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
<Route path="/publicfeed/:feedId" element={<PublicFeedPage />} />
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
|
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;
|
@@ -87,43 +87,59 @@ export const getAllFeeds = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFeed = async (feedId: string, name: string, isPublic: boolean) => {
|
||||
export const addNewFeed = async (name: string, isPublic: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) throw new Error('Authentication required');
|
||||
if (!token) {
|
||||
throw new Error("Authentication required");
|
||||
}
|
||||
|
||||
const response = await userDataApi.patch('/api/updatefeed', {
|
||||
feedId,
|
||||
const response = await userDataApi.post(
|
||||
"/api/addnewfeed",
|
||||
{
|
||||
Name: name,
|
||||
IsPublic: isPublic,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const addNewFeed = async (name: string, isPublic: boolean) => {
|
||||
const token = getToken();
|
||||
if (!token) throw new Error('Authentication required');
|
||||
|
||||
try {
|
||||
const response = await userDataApi.post('/api/addnewfeed', {
|
||||
Name: name,
|
||||
IsPublic: isPublic
|
||||
}, {
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}
|
||||
});
|
||||
);
|
||||
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 {
|
||||
const response = await userDataApi.delete('/api/deletefeed', {
|
||||
@@ -148,6 +164,23 @@ export const getFeedDataTimeRange = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublicFeedDataTimeRange = async (
|
||||
feedId: string,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
): Promise<{ feedName: string; data: { timestamp: string; data: string }[] }> => {
|
||||
try {
|
||||
const response = await dataApi.get(`/dash/getpublicfeeddatatimerange/${feedId}/${startTime}/${endTime}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("This feed is private or does not exist.");
|
||||
}
|
||||
throw new Error("Failed to fetch public feed data.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// API Key functions
|
||||
export const createApiKey = async () => {
|
||||
try {
|
||||
|
@@ -25,7 +25,7 @@ export interface ApiKeyResponse {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
// New ApiKey Typez
|
||||
// New ApiKey Type
|
||||
export interface ApiKey {
|
||||
createdAt: string;
|
||||
key: string;
|
||||
@@ -35,6 +35,13 @@ export interface ApiKey {
|
||||
export interface Feed {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export interface AddFeedRequest {
|
||||
Name: string;
|
||||
IsPublic: boolean;
|
||||
}
|
||||
|
||||
export interface AddFeedResponse {
|
||||
@@ -42,6 +49,7 @@ export interface AddFeedResponse {
|
||||
feedId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface FeedDataPoint {
|
||||
timestamp: string;
|
||||
data: string;
|
||||
|
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
|
||||
// Create a client for React Query
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -25,6 +26,7 @@ ReactDOM.createRoot(rootElement).render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<Toaster position="top-right" />
|
||||
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
@@ -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,34 +56,49 @@ export function DashboardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle add new feed
|
||||
const handleAddNewFeed = async () => {
|
||||
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 {
|
||||
const response = await addNewFeed();
|
||||
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);
|
||||
@@ -106,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" />
|
||||
@@ -155,25 +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 {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>
|
||||
@@ -184,16 +203,53 @@ export function DashboardPage() {
|
||||
<Card className="md:col-span-3 relative">
|
||||
<CardHeader className="flex flex-col md:flex-row md:items-center md:justify-between space-y-2 md:space-y-0">
|
||||
<div>
|
||||
<CardTitle>Feed Data</CardTitle>
|
||||
<CardDescription>
|
||||
{selectedFeed
|
||||
? `Showing data for Feed ${selectedFeed.id.substring(0, 8)}...`
|
||||
: "Select a feed to view data"
|
||||
}
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{selectedFeed ? `Feed Data: ${selectedFeed.name}` : "Feed Data"}
|
||||
</CardTitle>
|
||||
<CardDescription className="flex flex-col">
|
||||
{selectedFeed ? (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
Feed ID:
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-xs">{selectedFeed.id}</code>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-1"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(selectedFeed.id);
|
||||
toast.success("Feed ID copied to clipboard");
|
||||
}}
|
||||
title="Copy Feed ID"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{selectedFeed.isPublic && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs ml-2"
|
||||
onClick={() => {
|
||||
const shareUrl = `${window.location.origin}/publicfeed/${selectedFeed.id}`;
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast.success("Public share link copied");
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
)}
|
||||
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
"Select a feed to view data"
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
{/* Flex container for the select box and button */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={timeRangeValue}
|
||||
@@ -212,7 +268,6 @@ export function DashboardPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Button to refresh feed data */}
|
||||
<button
|
||||
className={`flex items-center p-1 border rounded-md
|
||||
${isLoading || !selectedFeed
|
||||
@@ -251,6 +306,14 @@ export function DashboardPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FeedDialog
|
||||
open={isDialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onSubmit={handleDialogSubmit}
|
||||
initialName={editFeedData?.name}
|
||||
initialIsPublic={editFeedData?.isPublic}
|
||||
mode={dialogMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
116
src/pages/PublicFeedPage.tsx
Normal file
116
src/pages/PublicFeedPage.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { getPublicFeedDataTimeRange } from "@/lib/api";
|
||||
import { formatDateTime, getTimeRangeOptions } from "@/lib/timeRanges";
|
||||
import { FeedChart } from "@/components/FeedChart";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ScanBarcode } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function PublicFeedPage() {
|
||||
const { feedId } = useParams<{ feedId: string }>();
|
||||
const [timeRangeValue, setTimeRangeValue] = useState("30min");
|
||||
const [feedName, setFeedName] = useState<string>("");
|
||||
const [data, setData] = useState<{ timestamp: string; data: string }[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
const fetchData = async (rangeValue: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const option = getTimeRangeOptions().find((o) => o.value === rangeValue);
|
||||
if (!option || !feedId) return;
|
||||
|
||||
const { startTime, endTime } = option.getTimeRange();
|
||||
const response = await getPublicFeedDataTimeRange(feedId, startTime, endTime);
|
||||
setFeedName(response.feedName);
|
||||
setData(response.data);
|
||||
setNotFound(false);
|
||||
} catch (error: any) {
|
||||
if (error.message.includes("private")) {
|
||||
setNotFound(true);
|
||||
} else {
|
||||
toast.error(error.message || "Error loading public feed");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(timeRangeValue);
|
||||
}, [feedId]);
|
||||
|
||||
const handleTimeRangeChange = (value: string) => {
|
||||
setTimeRangeValue(value);
|
||||
fetchData(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 max-w-4xl">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle>
|
||||
{notFound
|
||||
? "Feed Not Available"
|
||||
: feedName
|
||||
? `Public Feed: ${feedName}`
|
||||
: "Loading Feed..."}
|
||||
</CardTitle>
|
||||
{!notFound && (
|
||||
<CardDescription>Visualized public data (read-only)</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!notFound && (
|
||||
<Select value={timeRangeValue} onValueChange={handleTimeRangeChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select time range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getTimeRangeOptions().map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-[350px] w-full" />
|
||||
) : notFound ? (
|
||||
<div className="flex flex-col items-center justify-center h-[350px] text-center">
|
||||
<ScanBarcode className="mb-2 h-10 w-10 text-muted-foreground" />
|
||||
<h3 className="text-lg font-medium">Private or Invalid Feed</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This feed cannot be accessed publicly.
|
||||
</p>
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-[350px] text-center">
|
||||
<ScanBarcode className="mb-2 h-10 w-10 text-muted-foreground" />
|
||||
<h3 className="text-lg font-medium">No data available</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No public data found for the selected time range.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<FeedChart data={data} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/ApiKeyItem.tsx","./src/components/FeedActions.tsx","./src/components/FeedChart.tsx","./src/components/Header.tsx","./src/components/Layout.tsx","./src/components/ProtectedRoute.tsx","./src/components/ThemeToggle.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-time-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/hooks/use-toast.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/store.ts","./src/lib/theme-provider.tsx","./src/lib/timeRanges.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/pages/ApiKeysPage.tsx","./src/pages/DashboardPage.tsx","./src/pages/HomePage.tsx","./src/pages/LoginPage.tsx","./src/pages/ProfilePage.tsx","./src/pages/RegisterPage.tsx","./src/pages/VerifyEmail.tsx","./vite.config.ts"],"version":"5.6.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/ApiKeyItem.tsx","./src/components/FeedActions.tsx","./src/components/FeedChart.tsx","./src/components/FeedDialog.tsx","./src/components/Header.tsx","./src/components/Layout.tsx","./src/components/ProtectedRoute.tsx","./src/components/ThemeToggle.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/date-time-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/hooks/use-toast.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/store.ts","./src/lib/theme-provider.tsx","./src/lib/timeRanges.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/pages/ApiKeysPage.tsx","./src/pages/DashboardPage.tsx","./src/pages/HomePage.tsx","./src/pages/LoginPage.tsx","./src/pages/ProfilePage.tsx","./src/pages/PublicFeedPage.tsx","./src/pages/RegisterPage.tsx","./src/pages/VerifyEmail.tsx","./vite.config.ts"],"version":"5.6.3"}
|
Reference in New Issue
Block a user