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 { 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>
|
||||||
|
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();
|
const token = getToken();
|
||||||
if (!token) throw new Error('Authentication required');
|
if (!token) {
|
||||||
|
throw new Error("Authentication required");
|
||||||
|
}
|
||||||
|
|
||||||
const response = await userDataApi.patch('/api/updatefeed', {
|
const response = await userDataApi.post(
|
||||||
feedId,
|
"/api/addnewfeed",
|
||||||
|
{
|
||||||
Name: name,
|
Name: name,
|
||||||
IsPublic: isPublic,
|
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: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json'
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} 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) => {
|
export const deleteFeed = async (feedId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await userDataApi.delete('/api/deletefeed', {
|
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
|
// API Key functions
|
||||||
export const createApiKey = async () => {
|
export const createApiKey = async () => {
|
||||||
try {
|
try {
|
||||||
|
@@ -25,7 +25,7 @@ export interface ApiKeyResponse {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New ApiKey Typez
|
// New ApiKey Type
|
||||||
export interface ApiKey {
|
export interface ApiKey {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -35,6 +35,13 @@ export interface ApiKey {
|
|||||||
export interface Feed {
|
export interface Feed {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
name: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddFeedRequest {
|
||||||
|
Name: string;
|
||||||
|
IsPublic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddFeedResponse {
|
export interface AddFeedResponse {
|
||||||
@@ -42,6 +49,7 @@ export interface AddFeedResponse {
|
|||||||
feedId: string;
|
feedId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface FeedDataPoint {
|
export interface FeedDataPoint {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
data: string;
|
data: string;
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { isAuthenticated } from "@/lib/auth";
|
import { isAuthenticated } from "@/lib/auth";
|
||||||
import { useFeedStore } from "@/lib/store";
|
import { useFeedStore } from "@/lib/store";
|
||||||
import { addNewFeed } from "@/lib/api";
|
import { addNewFeed, updateFeed } from "@/lib/api";
|
||||||
import { formatDateTime, getTimeRangeOptions } from "@/lib/timeRanges";
|
import { formatDateTime, getTimeRangeOptions } from "@/lib/timeRanges";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { FeedChart } from "@/components/FeedChart";
|
||||||
import { FeedActions } from "@/components/FeedActions";
|
import { FeedActions } from "@/components/FeedActions";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { FeedDialog } from "@/components/FeedDialog";
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -36,7 +37,6 @@ export function DashboardPage() {
|
|||||||
deleteFeed,
|
deleteFeed,
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
// Check if user is authenticated
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated()) {
|
if (!isAuthenticated()) {
|
||||||
toast.error("Please login to access the dashboard");
|
toast.error("Please login to access the dashboard");
|
||||||
@@ -44,11 +44,9 @@ export function DashboardPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch feeds on component mount
|
|
||||||
fetchFeeds();
|
fetchFeeds();
|
||||||
}, [navigate, fetchFeeds]);
|
}, [navigate, fetchFeeds]);
|
||||||
|
|
||||||
// Handle time range change
|
|
||||||
const handleTimeRangeChange = (value: string) => {
|
const handleTimeRangeChange = (value: string) => {
|
||||||
setTimeRangeValue(value);
|
setTimeRangeValue(value);
|
||||||
const selectedOption = getTimeRangeOptions().find(option => option.value === value);
|
const selectedOption = getTimeRangeOptions().find(option => option.value === value);
|
||||||
@@ -58,34 +56,49 @@ export function DashboardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle add new feed
|
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||||
const handleAddNewFeed = async () => {
|
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 {
|
try {
|
||||||
const response = await addNewFeed();
|
if (dialogMode === "create") {
|
||||||
|
const response = await addNewFeed(name, isPublic);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success("New feed created successfully");
|
toast.success("Feed created successfully");
|
||||||
fetchFeeds();
|
fetchFeeds();
|
||||||
} else {
|
} else {
|
||||||
toast.error("Failed to create new feed");
|
toast.error(response.message || "Failed to create feed");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else if (dialogMode === "edit" && editFeedData) {
|
||||||
toast.error("An error occurred while creating a new feed");
|
const success = await updateFeed(editFeedData.id, name, isPublic);
|
||||||
console.error(error);
|
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 () => {
|
const handleRefresh = async () => {
|
||||||
// First refresh the feeds list
|
|
||||||
await fetchFeeds();
|
await fetchFeeds();
|
||||||
// Then refresh current feed data if a feed is selected
|
|
||||||
if (selectedFeed) {
|
if (selectedFeed) {
|
||||||
await refreshCurrentFeedData();
|
await refreshCurrentFeedData();
|
||||||
}
|
}
|
||||||
toast.success("Data refreshed");
|
toast.success("Data refreshed");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete feed
|
|
||||||
const handleDeleteFeed = async (feedId: string) => {
|
const handleDeleteFeed = async (feedId: string) => {
|
||||||
try {
|
try {
|
||||||
await deleteFeed(feedId);
|
await deleteFeed(feedId);
|
||||||
@@ -106,7 +119,6 @@ export function DashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Button to add a new feed */}
|
|
||||||
<Button size="sm" onClick={handleAddNewFeed} disabled={isLoading}>
|
<Button size="sm" onClick={handleAddNewFeed} disabled={isLoading}>
|
||||||
Add Feed
|
Add Feed
|
||||||
<Plus className="ml-2 h-4 w-4" />
|
<Plus className="ml-2 h-4 w-4" />
|
||||||
@@ -155,25 +167,32 @@ export function DashboardPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{feeds.map((feed) => (
|
{feeds.map((feed) => (
|
||||||
<div
|
<div key={feed.id} className="flex items-center justify-between border p-2 rounded">
|
||||||
key={feed.id}
|
<div className="cursor-pointer text-sm font-medium flex items-center gap-2" onClick={() => selectFeed(feed)}>
|
||||||
className="flex items-center gap-1"
|
{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
|
{feed.isPublic ? 'Public' : 'Private'}
|
||||||
variant={selectedFeed?.id === feed.id ? "default" : "outline"}
|
</span>
|
||||||
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)}...
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground truncate w-full">
|
|
||||||
Created: {formatDateTime(feed.createdAt)}
|
<div className="flex gap-1">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<FeedActions feed={feed} onDelete={handleDeleteFeed} />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -184,16 +203,53 @@ export function DashboardPage() {
|
|||||||
<Card className="md:col-span-3 relative">
|
<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">
|
<CardHeader className="flex flex-col md:flex-row md:items-center md:justify-between space-y-2 md:space-y-0">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Feed Data</CardTitle>
|
<CardHeader>
|
||||||
<CardDescription>
|
<CardTitle>
|
||||||
{selectedFeed
|
{selectedFeed ? `Feed Data: ${selectedFeed.name}` : "Feed Data"}
|
||||||
? `Showing data for Feed ${selectedFeed.id.substring(0, 8)}...`
|
</CardTitle>
|
||||||
: "Select a feed to view data"
|
<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>
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Flex container for the select box and button */}
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Select
|
<Select
|
||||||
value={timeRangeValue}
|
value={timeRangeValue}
|
||||||
@@ -212,7 +268,6 @@ export function DashboardPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Button to refresh feed data */}
|
|
||||||
<button
|
<button
|
||||||
className={`flex items-center p-1 border rounded-md
|
className={`flex items-center p-1 border rounded-md
|
||||||
${isLoading || !selectedFeed
|
${isLoading || !selectedFeed
|
||||||
@@ -251,6 +306,14 @@ export function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<FeedDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
onSubmit={handleDialogSubmit}
|
||||||
|
initialName={editFeedData?.name}
|
||||||
|
initialIsPublic={editFeedData?.isPublic}
|
||||||
|
mode={dialogMode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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