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 { 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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
export const createApiKey = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,8 @@ export interface ApiKey {
 | 
			
		||||
export interface Feed {
 | 
			
		||||
  id: string;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  isPublic: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddFeedRequest {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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