mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 17:01:02 +00:00
Integrate live blockchain data for governance features
Added: - useGovernance hook: Fetches live proposals and referenda from blockchain - useTreasury hook: Fetches live treasury balance and proposals - TreasuryOverview: Now uses real blockchain data with loading/error states - Forum database schema: Admin announcements, categories, discussions, replies, reactions Features: - Live data badge shows active blockchain connection - Automatic refresh every 30 seconds for treasury data - Secure RLS policies for forum access control - Admin announcements system with priority and expiry - Forum reactions (upvote/downvote) support Next: Complete forum UI with admin banner and moderation panel
This commit is contained in:
@@ -4,18 +4,21 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { useTreasury } from '@/hooks/useTreasury';
|
||||||
DollarSign,
|
import {
|
||||||
TrendingUp,
|
DollarSign,
|
||||||
TrendingDown,
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
PieChart,
|
PieChart,
|
||||||
Activity,
|
Activity,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ArrowDownRight
|
ArrowDownRight,
|
||||||
|
Loader2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface TreasuryMetrics {
|
interface TreasuryMetrics {
|
||||||
@@ -38,14 +41,7 @@ interface BudgetCategory {
|
|||||||
|
|
||||||
export const TreasuryOverview: React.FC = () => {
|
export const TreasuryOverview: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [metrics, setMetrics] = useState<TreasuryMetrics>({
|
const { metrics, proposals, loading, error } = useTreasury();
|
||||||
totalBalance: 2500000,
|
|
||||||
monthlyIncome: 150000,
|
|
||||||
monthlyExpenses: 120000,
|
|
||||||
pendingProposals: 8,
|
|
||||||
approvedBudget: 1800000,
|
|
||||||
healthScore: 85
|
|
||||||
});
|
|
||||||
|
|
||||||
const [categories] = useState<BudgetCategory[]>([
|
const [categories] = useState<BudgetCategory[]>([
|
||||||
{ id: '1', name: 'Development', allocated: 500000, spent: 320000, remaining: 180000, color: 'bg-blue-500' },
|
{ id: '1', name: 'Development', allocated: 500000, spent: 320000, remaining: 180000, color: 'bg-blue-500' },
|
||||||
@@ -66,8 +62,39 @@ export const TreasuryOverview: React.FC = () => {
|
|||||||
const healthStatus = getHealthStatus(metrics.healthScore);
|
const healthStatus = getHealthStatus(metrics.healthScore);
|
||||||
const HealthIcon = healthStatus.icon;
|
const HealthIcon = healthStatus.icon;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
<span className="ml-3 text-muted-foreground">Loading treasury data from blockchain...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Failed to load treasury data: {error}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Live Data Badge */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||||
|
<Activity className="h-3 w-3 mr-1" />
|
||||||
|
Live Blockchain Data
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{proposals.length} active proposals • {metrics.totalBalance.toFixed(2)} PZKW in treasury
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Treasury Health Score */}
|
{/* Treasury Health Score */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
|
||||||
|
export interface Proposal {
|
||||||
|
id: string;
|
||||||
|
proposalIndex: number;
|
||||||
|
hash: string;
|
||||||
|
proposer: string;
|
||||||
|
value: string;
|
||||||
|
beneficiary: string;
|
||||||
|
bond: string;
|
||||||
|
status: 'active' | 'approved' | 'rejected';
|
||||||
|
method: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Referendum {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
hash: string;
|
||||||
|
threshold: string;
|
||||||
|
delay: number;
|
||||||
|
end: number;
|
||||||
|
voteCount: number;
|
||||||
|
ayeVotes: string;
|
||||||
|
nayVotes: string;
|
||||||
|
status: 'ongoing' | 'passed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGovernance() {
|
||||||
|
const { api, isConnected } = usePolkadot();
|
||||||
|
const [proposals, setProposals] = useState<Proposal[]>([]);
|
||||||
|
const [referenda, setReferenda] = useState<Referendum[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api || !isConnected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchGovernanceData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Fetch Treasury Proposals
|
||||||
|
const proposalsData = await api.query.treasury?.proposals?.entries();
|
||||||
|
if (proposalsData) {
|
||||||
|
const parsedProposals: Proposal[] = proposalsData.map(([key, value]: any) => {
|
||||||
|
const proposalIndex = key.args[0].toNumber();
|
||||||
|
const proposal = value.unwrap();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `prop-${proposalIndex}`,
|
||||||
|
proposalIndex,
|
||||||
|
hash: key.toHex(),
|
||||||
|
proposer: proposal.proposer.toString(),
|
||||||
|
value: proposal.value.toString(),
|
||||||
|
beneficiary: proposal.beneficiary.toString(),
|
||||||
|
bond: proposal.bond.toString(),
|
||||||
|
status: 'active',
|
||||||
|
method: 'treasury.approveProposal',
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setProposals(parsedProposals);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Democracy Referenda
|
||||||
|
const referendaData = await api.query.democracy?.referendumInfoOf?.entries();
|
||||||
|
if (referendaData) {
|
||||||
|
const parsedReferenda: Referendum[] = referendaData.map(([key, value]: any) => {
|
||||||
|
const index = key.args[0].toNumber();
|
||||||
|
const info = value.unwrap();
|
||||||
|
|
||||||
|
if (info.isOngoing) {
|
||||||
|
const ongoing = info.asOngoing;
|
||||||
|
return {
|
||||||
|
id: `ref-${index}`,
|
||||||
|
index,
|
||||||
|
hash: key.toHex(),
|
||||||
|
threshold: ongoing.threshold.toString(),
|
||||||
|
delay: ongoing.delay.toNumber(),
|
||||||
|
end: ongoing.end.toNumber(),
|
||||||
|
voteCount: 0,
|
||||||
|
ayeVotes: ongoing.tally?.ayes?.toString() || '0',
|
||||||
|
nayVotes: ongoing.tally?.nays?.toString() || '0',
|
||||||
|
status: 'ongoing' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean) as Referendum[];
|
||||||
|
|
||||||
|
setReferenda(parsedReferenda);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching governance data:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch governance data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGovernanceData();
|
||||||
|
|
||||||
|
// Subscribe to updates
|
||||||
|
const interval = setInterval(fetchGovernanceData, 30000); // Refresh every 30 seconds
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [api, isConnected]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
proposals,
|
||||||
|
referenda,
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
|
||||||
|
export interface TreasuryMetrics {
|
||||||
|
totalBalance: number;
|
||||||
|
monthlyIncome: number;
|
||||||
|
monthlyExpenses: number;
|
||||||
|
pendingProposals: number;
|
||||||
|
approvedBudget: number;
|
||||||
|
healthScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreasuryProposal {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
proposer: string;
|
||||||
|
beneficiary: string;
|
||||||
|
value: string;
|
||||||
|
bond: string;
|
||||||
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTreasury() {
|
||||||
|
const { api, isConnected } = usePolkadot();
|
||||||
|
const [metrics, setMetrics] = useState<TreasuryMetrics>({
|
||||||
|
totalBalance: 0,
|
||||||
|
monthlyIncome: 0,
|
||||||
|
monthlyExpenses: 0,
|
||||||
|
pendingProposals: 0,
|
||||||
|
approvedBudget: 0,
|
||||||
|
healthScore: 0
|
||||||
|
});
|
||||||
|
const [proposals, setProposals] = useState<TreasuryProposal[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api || !isConnected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTreasuryData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Get treasury account balance
|
||||||
|
const treasuryAccount = await api.query.treasury?.treasury?.();
|
||||||
|
let totalBalance = 0;
|
||||||
|
|
||||||
|
if (treasuryAccount) {
|
||||||
|
totalBalance = parseInt(treasuryAccount.toString()) / 1e12; // Convert from planck to tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all treasury proposals
|
||||||
|
const proposalsData = await api.query.treasury?.proposals?.entries();
|
||||||
|
const proposalsList: TreasuryProposal[] = [];
|
||||||
|
let approvedBudget = 0;
|
||||||
|
let pendingCount = 0;
|
||||||
|
|
||||||
|
if (proposalsData) {
|
||||||
|
proposalsData.forEach(([key, value]: any) => {
|
||||||
|
const index = key.args[0].toNumber();
|
||||||
|
const proposal = value.unwrap();
|
||||||
|
const valueAmount = parseInt(proposal.value.toString()) / 1e12;
|
||||||
|
|
||||||
|
const proposalItem: TreasuryProposal = {
|
||||||
|
id: `treasury-${index}`,
|
||||||
|
index,
|
||||||
|
proposer: proposal.proposer.toString(),
|
||||||
|
beneficiary: proposal.beneficiary.toString(),
|
||||||
|
value: proposal.value.toString(),
|
||||||
|
bond: proposal.bond.toString(),
|
||||||
|
status: 'pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
proposalsList.push(proposalItem);
|
||||||
|
pendingCount++;
|
||||||
|
approvedBudget += valueAmount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate health score (simplified)
|
||||||
|
const healthScore = Math.min(100, Math.round((totalBalance / (approvedBudget || 1)) * 100));
|
||||||
|
|
||||||
|
setMetrics({
|
||||||
|
totalBalance,
|
||||||
|
monthlyIncome: 0, // This would require historical data
|
||||||
|
monthlyExpenses: 0, // This would require historical data
|
||||||
|
pendingProposals: pendingCount,
|
||||||
|
approvedBudget,
|
||||||
|
healthScore: isNaN(healthScore) ? 0 : healthScore
|
||||||
|
});
|
||||||
|
|
||||||
|
setProposals(proposalsList);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching treasury data:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch treasury data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTreasuryData();
|
||||||
|
|
||||||
|
// Subscribe to updates
|
||||||
|
const interval = setInterval(fetchTreasuryData, 30000); // Refresh every 30 seconds
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [api, isConnected]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metrics,
|
||||||
|
proposals,
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
-- Forum Tables for Pezkuwi Governance
|
||||||
|
|
||||||
|
-- Admin Announcements Table
|
||||||
|
CREATE TABLE IF NOT EXISTS public.admin_announcements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL DEFAULT 'info', -- 'info', 'warning', 'success', 'critical'
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
priority INTEGER DEFAULT 0, -- Higher priority shows first
|
||||||
|
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Categories
|
||||||
|
CREATE TABLE IF NOT EXISTS public.forum_categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
icon TEXT,
|
||||||
|
color TEXT,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Discussions
|
||||||
|
CREATE TABLE IF NOT EXISTS public.forum_discussions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
category_id UUID REFERENCES public.forum_categories(id) ON DELETE SET NULL,
|
||||||
|
proposal_id TEXT, -- Link to blockchain proposal if applicable
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
author_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
author_name TEXT NOT NULL,
|
||||||
|
author_address TEXT, -- Blockchain address
|
||||||
|
is_pinned BOOLEAN DEFAULT false,
|
||||||
|
is_locked BOOLEAN DEFAULT false,
|
||||||
|
views_count INTEGER DEFAULT 0,
|
||||||
|
replies_count INTEGER DEFAULT 0,
|
||||||
|
tags TEXT[],
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Replies
|
||||||
|
CREATE TABLE IF NOT EXISTS public.forum_replies (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discussion_id UUID REFERENCES public.forum_discussions(id) ON DELETE CASCADE,
|
||||||
|
parent_reply_id UUID REFERENCES public.forum_replies(id) ON DELETE CASCADE, -- For nested replies
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
author_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
author_name TEXT NOT NULL,
|
||||||
|
author_address TEXT,
|
||||||
|
is_edited BOOLEAN DEFAULT false,
|
||||||
|
edited_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Discussion Reactions (upvotes/downvotes)
|
||||||
|
CREATE TABLE IF NOT EXISTS public.forum_reactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discussion_id UUID REFERENCES public.forum_discussions(id) ON DELETE CASCADE,
|
||||||
|
reply_id UUID REFERENCES public.forum_replies(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
reaction_type TEXT NOT NULL, -- 'upvote', 'downvote', 'helpful', etc.
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE(discussion_id, user_id, reaction_type),
|
||||||
|
UNIQUE(reply_id, user_id, reaction_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_announcements_active ON public.admin_announcements(is_active, priority DESC, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_discussions_category ON public.forum_discussions(category_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_discussions_proposal ON public.forum_discussions(proposal_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_discussions_pinned ON public.forum_discussions(is_pinned DESC, last_activity_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_replies_discussion ON public.forum_replies(discussion_id, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reactions_discussion ON public.forum_reactions(discussion_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reactions_reply ON public.forum_reactions(reply_id);
|
||||||
|
|
||||||
|
-- RLS Policies
|
||||||
|
|
||||||
|
-- Admin Announcements
|
||||||
|
ALTER TABLE public.admin_announcements ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "Anyone can view active announcements"
|
||||||
|
ON public.admin_announcements FOR SELECT
|
||||||
|
USING (is_active = true AND (expires_at IS NULL OR expires_at > NOW()));
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage announcements"
|
||||||
|
ON public.admin_announcements FOR ALL
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Categories
|
||||||
|
ALTER TABLE public.forum_categories ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "Anyone can view active categories"
|
||||||
|
ON public.forum_categories FOR SELECT
|
||||||
|
USING (is_active = true);
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage categories"
|
||||||
|
ON public.forum_categories FOR ALL
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Discussions
|
||||||
|
ALTER TABLE public.forum_discussions ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "Anyone can view discussions"
|
||||||
|
ON public.forum_discussions FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Authenticated users can create discussions"
|
||||||
|
ON public.forum_discussions FOR INSERT
|
||||||
|
WITH CHECK (auth.uid() IS NOT NULL);
|
||||||
|
|
||||||
|
CREATE POLICY "Authors and admins can update discussions"
|
||||||
|
ON public.forum_discussions FOR UPDATE
|
||||||
|
USING (
|
||||||
|
author_id = auth.uid() OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Authors and admins can delete discussions"
|
||||||
|
ON public.forum_discussions FOR DELETE
|
||||||
|
USING (
|
||||||
|
author_id = auth.uid() OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Replies
|
||||||
|
ALTER TABLE public.forum_replies ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "Anyone can view replies"
|
||||||
|
ON public.forum_replies FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Authenticated users can create replies"
|
||||||
|
ON public.forum_replies FOR INSERT
|
||||||
|
WITH CHECK (auth.uid() IS NOT NULL);
|
||||||
|
|
||||||
|
CREATE POLICY "Authors and admins can update replies"
|
||||||
|
ON public.forum_replies FOR UPDATE
|
||||||
|
USING (
|
||||||
|
author_id = auth.uid() OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Authors and admins can delete replies"
|
||||||
|
ON public.forum_replies FOR DELETE
|
||||||
|
USING (
|
||||||
|
author_id = auth.uid() OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.admin_roles
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Forum Reactions
|
||||||
|
ALTER TABLE public.forum_reactions ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "Anyone can view reactions"
|
||||||
|
ON public.forum_reactions FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Authenticated users can manage own reactions"
|
||||||
|
ON public.forum_reactions FOR ALL
|
||||||
|
USING (user_id = auth.uid());
|
||||||
|
|
||||||
|
-- Insert Default Categories
|
||||||
|
INSERT INTO public.forum_categories (name, description, icon, color, display_order) VALUES
|
||||||
|
('Treasury', 'Discussions about treasury proposals and funding', '💰', '#10B981', 1),
|
||||||
|
('Technical', 'Technical discussions and protocol upgrades', '⚙️', '#3B82F6', 2),
|
||||||
|
('Governance', 'Governance proposals and voting discussions', '🗳️', '#8B5CF6', 3),
|
||||||
|
('Community', 'Community initiatives and general discussions', '👥', '#F59E0B', 4),
|
||||||
|
('Support', 'Help and support for using Pezkuwi', '❓', '#6B7280', 5)
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
|
||||||
|
-- Function to update last_activity
|
||||||
|
CREATE OR REPLACE FUNCTION update_discussion_activity()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.forum_discussions
|
||||||
|
SET
|
||||||
|
last_activity_at = NOW(),
|
||||||
|
replies_count = replies_count + 1
|
||||||
|
WHERE id = NEW.discussion_id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER on_reply_created
|
||||||
|
AFTER INSERT ON public.forum_replies
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_discussion_activity();
|
||||||
Reference in New Issue
Block a user