Enable management of media outlet articles including creation, editing, and deletion
Adds article management functionality to media outlets, including API endpoints for creating, updating, and deleting articles, and integrates these features into the client-side UI with dialogs and mutations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/QTw0kIA
This commit is contained in:
4
.replit
4
.replit
@ -18,6 +18,10 @@ externalPort = 80
|
|||||||
localPort = 34047
|
localPort = 34047
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 34595
|
||||||
|
externalPort = 6800
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 36309
|
localPort = 36309
|
||||||
externalPort = 5173
|
externalPort = 5173
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare } from "lucide-react";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
import type { MediaOutlet } from "@shared/schema";
|
import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare, Trash2, Eye } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||||
|
import type { MediaOutlet, Article } from "@shared/schema";
|
||||||
|
|
||||||
interface MediaOutletManagementProps {
|
interface MediaOutletManagementProps {
|
||||||
outlet: MediaOutlet;
|
outlet: MediaOutlet;
|
||||||
@ -14,12 +18,127 @@ interface MediaOutletManagementProps {
|
|||||||
|
|
||||||
export default function MediaOutletManagement({ outlet, onBack }: MediaOutletManagementProps) {
|
export default function MediaOutletManagement({ outlet, onBack }: MediaOutletManagementProps) {
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
|
const [isArticleDialogOpen, setIsArticleDialogOpen] = useState(false);
|
||||||
// TODO: Add queries for articles, predictions, auctions, comments related to this outlet
|
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
|
||||||
const { data: articles = [] } = useQuery({
|
const [articleForm, setArticleForm] = useState({
|
||||||
queryKey: ["/api/articles", outlet.id],
|
title: "",
|
||||||
enabled: false, // Disabled for now as articles API is not implemented
|
slug: "",
|
||||||
|
content: "",
|
||||||
|
excerpt: "",
|
||||||
|
imageUrl: "",
|
||||||
|
publishedMinutesAgo: 0,
|
||||||
|
isFeatured: false
|
||||||
});
|
});
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { data: articles = [], isLoading: articlesLoading } = useQuery<Article[]>({
|
||||||
|
queryKey: [`/api/media-outlets/${outlet.slug}/articles`],
|
||||||
|
});
|
||||||
|
|
||||||
|
const createArticleMutation = useMutation({
|
||||||
|
mutationFn: async (data: typeof articleForm) => {
|
||||||
|
return await apiRequest("/api/articles", "POST", { ...data, mediaOutletId: outlet.id });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||||
|
setIsArticleDialogOpen(false);
|
||||||
|
resetArticleForm();
|
||||||
|
toast({
|
||||||
|
title: "Article Created",
|
||||||
|
description: "The article has been created successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to create article",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateArticleMutation = useMutation({
|
||||||
|
mutationFn: async (data: { id: string; updates: Partial<typeof articleForm> }) => {
|
||||||
|
return await apiRequest(`/api/articles/${data.id}`, "PATCH", data.updates);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||||
|
setIsArticleDialogOpen(false);
|
||||||
|
setEditingArticle(null);
|
||||||
|
resetArticleForm();
|
||||||
|
toast({
|
||||||
|
title: "Article Updated",
|
||||||
|
description: "The article has been updated successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to update article",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteArticleMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
return await apiRequest(`/api/articles/${id}`, "DELETE", {});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||||
|
toast({
|
||||||
|
title: "Article Deleted",
|
||||||
|
description: "The article has been deleted successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to delete article",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetArticleForm = () => {
|
||||||
|
setArticleForm({
|
||||||
|
title: "",
|
||||||
|
slug: "",
|
||||||
|
content: "",
|
||||||
|
excerpt: "",
|
||||||
|
imageUrl: "",
|
||||||
|
publishedMinutesAgo: 0,
|
||||||
|
isFeatured: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNewArticleDialog = () => {
|
||||||
|
resetArticleForm();
|
||||||
|
setEditingArticle(null);
|
||||||
|
setIsArticleDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditArticleDialog = (article: Article) => {
|
||||||
|
setArticleForm({
|
||||||
|
title: article.title,
|
||||||
|
slug: article.slug,
|
||||||
|
content: article.content || "",
|
||||||
|
excerpt: article.excerpt || "",
|
||||||
|
imageUrl: article.imageUrl || "",
|
||||||
|
publishedMinutesAgo: article.publishedMinutesAgo,
|
||||||
|
isFeatured: article.isFeatured || false
|
||||||
|
});
|
||||||
|
setEditingArticle(article);
|
||||||
|
setIsArticleDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitArticle = () => {
|
||||||
|
if (editingArticle) {
|
||||||
|
updateArticleMutation.mutate({ id: editingArticle.id, updates: articleForm });
|
||||||
|
} else {
|
||||||
|
createArticleMutation.mutate(articleForm);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
@ -84,7 +203,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Total Articles</h3>
|
<h3 className="text-sm font-medium text-gray-500">Total Articles</h3>
|
||||||
<p className="text-2xl font-bold">0</p>
|
<p className="text-2xl font-bold">{articles.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-blue-600">
|
<div className="text-blue-600">
|
||||||
<BarChart3 className="h-6 w-6" />
|
<BarChart3 className="h-6 w-6" />
|
||||||
@ -207,18 +326,90 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>Article Management</CardTitle>
|
<CardTitle>Article Management ({articles.length})</CardTitle>
|
||||||
<Button data-testid="button-new-article">
|
<Button onClick={openNewArticleDialog} data-testid="button-new-article">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create New Article
|
Create New Article
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-center py-12">
|
{articlesLoading ? (
|
||||||
<div className="text-gray-400 text-lg mb-2">No articles yet</div>
|
<div className="space-y-3">
|
||||||
<div className="text-gray-500 text-sm">Create your first article</div>
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
</div>
|
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 border rounded-lg">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="h-4 bg-gray-200 rounded mb-2 w-3/4"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="h-8 w-16 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-8 w-16 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : articles.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-400 text-lg mb-2">No articles yet</div>
|
||||||
|
<div className="text-gray-500 text-sm">Create your first article</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{articles.map((article) => (
|
||||||
|
<div
|
||||||
|
key={article.id}
|
||||||
|
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition"
|
||||||
|
data-testid={`article-item-${article.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h3 className="font-medium text-gray-900 truncate">{article.title}</h3>
|
||||||
|
{article.isFeatured && (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">
|
||||||
|
Featured
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 truncate">{article.excerpt || "No excerpt"}</p>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
|
Published {article.publishedMinutesAgo} minutes ago • Slug: {article.slug}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 ml-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => window.open(`/articles/${article.slug}`, '_blank')}
|
||||||
|
data-testid={`button-view-${article.id}`}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openEditArticleDialog(article)}
|
||||||
|
data-testid={`button-edit-${article.id}`}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm('Are you sure you want to delete this article?')) {
|
||||||
|
deleteArticleMutation.mutate(article.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid={`button-delete-${article.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-red-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@ -278,6 +469,112 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Article Dialog */}
|
||||||
|
<Dialog open={isArticleDialogOpen} onOpenChange={setIsArticleDialogOpen}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingArticle ? "Edit Article" : "Create New Article"}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{editingArticle ? "Update the article details below." : "Fill in the details to create a new article."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Title</label>
|
||||||
|
<Input
|
||||||
|
value={articleForm.title}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, title: e.target.value })}
|
||||||
|
placeholder="Enter article title"
|
||||||
|
data-testid="input-article-title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Slug</label>
|
||||||
|
<Input
|
||||||
|
value={articleForm.slug}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, slug: e.target.value })}
|
||||||
|
placeholder="article-slug-url"
|
||||||
|
data-testid="input-article-slug"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">URL-friendly identifier (e.g., my-article-title)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Excerpt</label>
|
||||||
|
<Textarea
|
||||||
|
value={articleForm.excerpt}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, excerpt: e.target.value })}
|
||||||
|
placeholder="Brief summary of the article"
|
||||||
|
rows={2}
|
||||||
|
data-testid="input-article-excerpt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Content</label>
|
||||||
|
<Textarea
|
||||||
|
value={articleForm.content}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, content: e.target.value })}
|
||||||
|
placeholder="Article content (supports markdown)"
|
||||||
|
rows={8}
|
||||||
|
data-testid="input-article-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Image URL</label>
|
||||||
|
<Input
|
||||||
|
value={articleForm.imageUrl}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, imageUrl: e.target.value })}
|
||||||
|
placeholder="https://example.com/image.jpg"
|
||||||
|
data-testid="input-article-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">Published Minutes Ago</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={articleForm.publishedMinutesAgo}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, publishedMinutesAgo: parseInt(e.target.value) || 0 })}
|
||||||
|
placeholder="0"
|
||||||
|
data-testid="input-article-minutes"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">How many minutes ago was this published</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isFeatured"
|
||||||
|
checked={articleForm.isFeatured}
|
||||||
|
onChange={(e) => setArticleForm({ ...articleForm, isFeatured: e.target.checked })}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
data-testid="input-article-featured"
|
||||||
|
/>
|
||||||
|
<label htmlFor="isFeatured" className="text-sm font-medium text-gray-700">
|
||||||
|
Featured Article
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2 mt-6">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsArticleDialogOpen(false);
|
||||||
|
setEditingArticle(null);
|
||||||
|
resetArticleForm();
|
||||||
|
}}
|
||||||
|
data-testid="button-cancel-article"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmitArticle}
|
||||||
|
disabled={!articleForm.title || !articleForm.slug || createArticleMutation.isPending || updateArticleMutation.isPending}
|
||||||
|
data-testid="button-save-article"
|
||||||
|
>
|
||||||
|
{createArticleMutation.isPending || updateArticleMutation.isPending ? "Saving..." : editingArticle ? "Update Article" : "Create Article"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -152,6 +152,49 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.patch('/api/articles/:id', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
if (!user || (user.role !== 'admin' && user.role !== 'superadmin')) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const article = await storage.getArticleById(req.params.id);
|
||||||
|
if (!article) {
|
||||||
|
return res.status(404).json({ message: "Article not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleData = insertArticleSchema.partial().parse(req.body);
|
||||||
|
const updated = await storage.updateArticle(req.params.id, articleData);
|
||||||
|
res.json(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating article:", error);
|
||||||
|
res.status(500).json({ message: "Failed to update article" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/articles/:id', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
if (!user || (user.role !== 'admin' && user.role !== 'superadmin')) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const article = await storage.getArticleById(req.params.id);
|
||||||
|
if (!article) {
|
||||||
|
return res.status(404).json({ message: "Article not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.deleteArticle(req.params.id);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting article:", error);
|
||||||
|
res.status(500).json({ message: "Failed to delete article" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Prediction market routes
|
// Prediction market routes
|
||||||
app.get('/api/prediction-markets', async (req, res) => {
|
app.get('/api/prediction-markets', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -52,8 +52,10 @@ export interface IStorage {
|
|||||||
getArticles(): Promise<Article[]>;
|
getArticles(): Promise<Article[]>;
|
||||||
getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>;
|
getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>;
|
||||||
getArticleBySlug(slug: string): Promise<Article | undefined>;
|
getArticleBySlug(slug: string): Promise<Article | undefined>;
|
||||||
|
getArticleById(id: string): Promise<Article | undefined>;
|
||||||
createArticle(article: InsertArticle): Promise<Article>;
|
createArticle(article: InsertArticle): Promise<Article>;
|
||||||
updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article>;
|
updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article>;
|
||||||
|
deleteArticle(id: string): Promise<void>;
|
||||||
getFeaturedArticles(limit?: number): Promise<Article[]>;
|
getFeaturedArticles(limit?: number): Promise<Article[]>;
|
||||||
|
|
||||||
// Prediction market operations
|
// Prediction market operations
|
||||||
@ -206,6 +208,15 @@ export class DatabaseStorage implements IStorage {
|
|||||||
.limit(limit);
|
.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getArticleById(id: string): Promise<Article | undefined> {
|
||||||
|
const [article] = await db.select().from(articles).where(eq(articles.id, id));
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteArticle(id: string): Promise<void> {
|
||||||
|
await db.delete(articles).where(eq(articles.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
// Prediction market operations
|
// Prediction market operations
|
||||||
async getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]> {
|
async getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]> {
|
||||||
if (articleId) {
|
if (articleId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user