Add public feeds page
This commit is contained in:
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 { ApiKeysPage } from "@/pages/ApiKeysPage";
|
||||||
import { ProfilePage } from "@/pages/ProfilePage";
|
import { ProfilePage } from "@/pages/ProfilePage";
|
||||||
import { VerifyEmail } from "@/pages/VerifyEmail";
|
import { VerifyEmail } from "@/pages/VerifyEmail";
|
||||||
|
import { PublicFeedPage } from "@/pages/PublicFeedPage";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +55,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
<Route path="/publicfeed/:feedId" element={<PublicFeedPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
@@ -164,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
|
// API Key functions
|
||||||
export const createApiKey = async () => {
|
export const createApiKey = async () => {
|
||||||
try {
|
try {
|
||||||
|
@@ -35,6 +35,8 @@ export interface ApiKey {
|
|||||||
export interface Feed {
|
export interface Feed {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
name: string;
|
||||||
|
isPublic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddFeedRequest {
|
export interface AddFeedRequest {
|
||||||
|
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
|
|
||||||
// Create a client for React Query
|
// Create a client for React Query
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -25,6 +26,7 @@ ReactDOM.createRoot(rootElement).render(
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
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