feat: District Portals Frontend
Pages: - /districts - list of all districts with cards - /districts/[slug] - district detail page - /soul, /greenfood, /energy-union - shortcut redirects UI Features: - District-specific colors and icons - Lead agent + core team display - District rooms list with Matrix status - District nodes list - Chat widget for lobby room - Stats (agents, rooms, nodes count)
This commit is contained in:
374
apps/web/src/app/districts/[slug]/page.tsx
Normal file
374
apps/web/src/app/districts/[slug]/page.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import {
|
||||
ArrowLeft, Building2, Bot, Users, MessageSquare,
|
||||
Home, Zap, Leaf, Heart, Server, Activity
|
||||
} from 'lucide-react'
|
||||
import { CityChatWidget } from '@/components/city/CityChatWidget'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface DistrictAgent {
|
||||
id: string
|
||||
name: string
|
||||
kind: string | null
|
||||
status: string | null
|
||||
avatar_url: string | null
|
||||
role: string | null
|
||||
is_core?: boolean
|
||||
}
|
||||
|
||||
interface DistrictRoom {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string | null
|
||||
matrix_room_id: string | null
|
||||
room_role: string | null
|
||||
is_public: boolean
|
||||
}
|
||||
|
||||
interface DistrictNode {
|
||||
id: string
|
||||
name: string
|
||||
kind: string | null
|
||||
status: string | null
|
||||
location: string | null
|
||||
}
|
||||
|
||||
interface DistrictDetail {
|
||||
district: {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string | null
|
||||
dao_type: string
|
||||
}
|
||||
lead_agent: DistrictAgent | null
|
||||
core_team: DistrictAgent[]
|
||||
agents: DistrictAgent[]
|
||||
rooms: DistrictRoom[]
|
||||
nodes: DistrictNode[]
|
||||
stats: {
|
||||
agents_count: number
|
||||
rooms_count: number
|
||||
nodes_count: number
|
||||
}
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>
|
||||
}
|
||||
|
||||
const districtIcons: Record<string, typeof Building2> = {
|
||||
'soul': Heart,
|
||||
'greenfood': Leaf,
|
||||
'energy-union': Zap,
|
||||
}
|
||||
|
||||
const districtColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||
'soul': { bg: 'from-purple-500/20 to-pink-600/20', text: 'text-purple-400', border: 'border-purple-500/30' },
|
||||
'greenfood': { bg: 'from-emerald-500/20 to-green-600/20', text: 'text-emerald-400', border: 'border-emerald-500/30' },
|
||||
'energy-union': { bg: 'from-amber-500/20 to-orange-600/20', text: 'text-amber-400', border: 'border-amber-500/30' },
|
||||
}
|
||||
|
||||
async function getDistrict(slug: string): Promise<DistrictDetail | null> {
|
||||
try {
|
||||
const apiUrl = process.env.INTERNAL_API_URL || 'http://daarion-city-service:7001'
|
||||
const res = await fetch(`${apiUrl}/api/v1/districts/${slug}`, {
|
||||
cache: 'no-store'
|
||||
})
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch district:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DistrictPage({ params }: PageProps) {
|
||||
const { slug } = await params
|
||||
const data = await getDistrict(slug)
|
||||
|
||||
if (!data) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { district, lead_agent, core_team, agents, rooms, nodes, stats } = data
|
||||
const Icon = districtIcons[slug] || Building2
|
||||
const colors = districtColors[slug] || { bg: 'from-cyan-500/20 to-blue-600/20', text: 'text-cyan-400', border: 'border-cyan-500/30' }
|
||||
|
||||
// Find lobby room for chat
|
||||
const lobbyRoom = rooms.find(r => r.slug.includes('lobby'))
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header */}
|
||||
<div className={`px-4 py-8 border-b border-white/5 bg-gradient-to-br ${colors.bg}`}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-sm mb-6">
|
||||
<Link href="/" className="text-slate-400 hover:text-white transition-colors">
|
||||
<Home className="w-4 h-4" />
|
||||
</Link>
|
||||
<span className="text-slate-600">/</span>
|
||||
<Link href="/districts" className="text-slate-400 hover:text-white transition-colors">
|
||||
Districts
|
||||
</Link>
|
||||
<span className="text-slate-600">/</span>
|
||||
<span className={colors.text}>{district.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Title */}
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-4 rounded-2xl bg-gradient-to-br ${colors.bg} border ${colors.border}`}>
|
||||
<Icon className={`w-10 h-10 ${colors.text}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold text-white">{district.name}</h1>
|
||||
<span className={`px-3 py-1 text-xs font-medium rounded-full bg-white/10 ${colors.text}`}>
|
||||
District
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-400 max-w-2xl">
|
||||
{district.description || 'District платформа DAARION.city'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-6 mt-6">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Bot className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-white font-medium">{stats.agents_count}</span>
|
||||
<span className="text-slate-400">агентів</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MessageSquare className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-white font-medium">{stats.rooms_count}</span>
|
||||
<span className="text-slate-400">кімнат</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Server className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-white font-medium">{stats.nodes_count}</span>
|
||||
<span className="text-slate-400">нод</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column - Chat */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Chat Widget */}
|
||||
{lobbyRoom && (
|
||||
<div className="glass-panel overflow-hidden">
|
||||
<div className="p-4 border-b border-white/10">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<MessageSquare className={`w-5 h-5 ${colors.text}`} />
|
||||
{lobbyRoom.name}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
Головна кімната District-а
|
||||
</p>
|
||||
</div>
|
||||
<CityChatWidget
|
||||
roomSlug={lobbyRoom.slug}
|
||||
mode="embedded"
|
||||
className="h-[500px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rooms */}
|
||||
{rooms.length > 0 && (
|
||||
<div className="glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Building2 className={`w-5 h-5 ${colors.text}`} />
|
||||
Кімнати District-а
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{rooms.map((room) => (
|
||||
<Link
|
||||
key={room.id}
|
||||
href={`/city/${room.slug}`}
|
||||
className="p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-white group-hover:text-cyan-400 transition-colors">
|
||||
{room.name}
|
||||
</p>
|
||||
{room.room_role && (
|
||||
<p className="text-xs text-slate-400 capitalize mt-1">
|
||||
{room.room_role}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{room.matrix_room_id && (
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400" title="Matrix connected" />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nodes */}
|
||||
{nodes.length > 0 && (
|
||||
<div className="glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Server className={`w-5 h-5 ${colors.text}`} />
|
||||
Ноди District-а
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{nodes.map((node) => (
|
||||
<Link
|
||||
key={node.id}
|
||||
href={`/nodes/${node.id}`}
|
||||
className="flex items-center justify-between p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="w-5 h-5 text-slate-400" />
|
||||
<div>
|
||||
<p className="font-medium text-white group-hover:text-cyan-400 transition-colors">
|
||||
{node.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
{node.kind} • {node.location || 'Unknown location'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
node.status === 'online'
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
: 'bg-slate-500/20 text-slate-400'
|
||||
}`}>
|
||||
{node.status || 'unknown'}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Agents */}
|
||||
<div className="space-y-6">
|
||||
{/* Lead Agent */}
|
||||
{lead_agent && (
|
||||
<div className="glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Bot className={`w-5 h-5 ${colors.text}`} />
|
||||
Lead Agent
|
||||
</h3>
|
||||
<Link
|
||||
href={`/agents/${lead_agent.id}`}
|
||||
className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-br from-white/5 to-white/10 hover:from-white/10 hover:to-white/15 transition-colors group"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${colors.bg} flex items-center justify-center`}>
|
||||
{lead_agent.avatar_url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={lead_agent.avatar_url}
|
||||
alt={lead_agent.name}
|
||||
className="w-full h-full rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Bot className={`w-7 h-7 ${colors.text}`} />
|
||||
)}
|
||||
</div>
|
||||
<span className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-slate-900 ${
|
||||
lead_agent.status === 'active' ? 'bg-emerald-400' : 'bg-slate-500'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-white group-hover:text-cyan-400 transition-colors">
|
||||
{lead_agent.name}
|
||||
</p>
|
||||
<p className="text-sm text-slate-400 capitalize">
|
||||
{lead_agent.kind || 'Agent'} • District Lead
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Core Team */}
|
||||
{core_team.length > 0 && (
|
||||
<div className="glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Users className={`w-5 h-5 ${colors.text}`} />
|
||||
Core Team
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{core_team.map((agent) => (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={`/agents/${agent.id}`}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition-colors group"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500/30 to-purple-600/30 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-violet-400" />
|
||||
</div>
|
||||
<span className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-slate-900 ${
|
||||
agent.status === 'active' ? 'bg-emerald-400' : 'bg-slate-500'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate group-hover:text-cyan-400 transition-colors">
|
||||
{agent.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 capitalize">
|
||||
{agent.kind || 'agent'}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* All Agents */}
|
||||
{agents.length > 0 && (
|
||||
<div className="glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Activity className={`w-5 h-5 ${colors.text}`} />
|
||||
Всі агенти ({agents.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{agents.map((agent) => (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={`/agents/${agent.id}`}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
agent.status === 'active' ? 'bg-emerald-400' : 'bg-slate-500'
|
||||
}`} />
|
||||
<span className="text-sm text-white group-hover:text-cyan-400 transition-colors">
|
||||
{agent.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 capitalize">
|
||||
{agent.role || agent.kind}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
196
apps/web/src/app/districts/page.tsx
Normal file
196
apps/web/src/app/districts/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Building2, Users, Bot, ArrowRight, Loader2, MapPin, Zap, Leaf, Heart } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DistrictSummary {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string | null
|
||||
dao_type: string
|
||||
lead_agent: {
|
||||
id: string
|
||||
name: string
|
||||
avatar_url: string | null
|
||||
} | null
|
||||
rooms_count: number
|
||||
rooms: { id: string; slug: string; name: string }[]
|
||||
}
|
||||
|
||||
const districtIcons: Record<string, typeof Building2> = {
|
||||
'soul': Heart,
|
||||
'greenfood': Leaf,
|
||||
'energy-union': Zap,
|
||||
}
|
||||
|
||||
const districtColors: Record<string, string> = {
|
||||
'soul': 'from-purple-500/20 to-pink-600/20 border-purple-500/30',
|
||||
'greenfood': 'from-emerald-500/20 to-green-600/20 border-emerald-500/30',
|
||||
'energy-union': 'from-amber-500/20 to-orange-600/20 border-amber-500/30',
|
||||
}
|
||||
|
||||
export default function DistrictsPage() {
|
||||
const [districts, setDistricts] = useState<DistrictSummary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDistricts() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/districts')
|
||||
if (!res.ok) throw new Error('Failed to fetch districts')
|
||||
const data = await res.json()
|
||||
setDistricts(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch districts:', err)
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchDistricts()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-cyan-400 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-400 mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-cyan-500/20 text-cyan-400 rounded-lg hover:bg-cyan-500/30"
|
||||
>
|
||||
Спробувати знову
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 rounded-xl bg-gradient-to-br from-violet-500/20 to-purple-600/20">
|
||||
<MapPin className="w-8 h-8 text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">District Portals</h1>
|
||||
<p className="text-slate-400">Платформи та спільноти DAARION.city</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Districts Grid */}
|
||||
{districts.length === 0 ? (
|
||||
<div className="glass-panel p-12 text-center">
|
||||
<Building2 className="w-16 h-16 text-slate-600 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-white mb-2">
|
||||
District-и не знайдено
|
||||
</h2>
|
||||
<p className="text-slate-400">
|
||||
Поки що немає активних District-ів
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{districts.map((district) => (
|
||||
<DistrictCard key={district.id} district={district} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-12 glass-panel p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Що таке District?</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">
|
||||
District — це платформа або тематична спільнота всередині DAARION.city.
|
||||
Кожен District має свого Lead Agent-а, команду агентів, власні кімнати
|
||||
та може містити MicroDAO. Districts є точками входу для користувачів
|
||||
у відповідні екосистеми: wellness (SOUL), food systems (GREENFOOD),
|
||||
energy/compute (ENERGY UNION).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DistrictCard({ district }: { district: DistrictSummary }) {
|
||||
const Icon = districtIcons[district.slug] || Building2
|
||||
const colorClass = districtColors[district.slug] || 'from-cyan-500/20 to-blue-600/20 border-cyan-500/30'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/districts/${district.slug}`}
|
||||
className={cn(
|
||||
"glass-panel p-6 group block transition-all hover:scale-[1.02]",
|
||||
"bg-gradient-to-br border",
|
||||
colorClass
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"p-3 rounded-xl bg-gradient-to-br",
|
||||
colorClass
|
||||
)}>
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-cyan-400 transition-colors">
|
||||
{district.name}
|
||||
</h3>
|
||||
<span className="text-xs text-slate-400 uppercase tracking-wider">
|
||||
District
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-slate-400 mb-4 line-clamp-2">
|
||||
{district.description || 'Без опису'}
|
||||
</p>
|
||||
|
||||
{/* Lead Agent */}
|
||||
{district.lead_agent && (
|
||||
<div className="flex items-center gap-2 mb-4 p-2 rounded-lg bg-white/5">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500/30 to-purple-600/30 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white font-medium">{district.lead_agent.name}</p>
|
||||
<p className="text-xs text-slate-400">Lead Agent</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-4 text-sm text-slate-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Building2 className="w-4 h-4" />
|
||||
{district.rooms_count} кімнат
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-slate-500 group-hover:text-cyan-400 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
6
apps/web/src/app/energy-union/page.tsx
Normal file
6
apps/web/src/app/energy-union/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function EnergyUnionPage() {
|
||||
redirect('/districts/energy-union')
|
||||
}
|
||||
|
||||
6
apps/web/src/app/greenfood/page.tsx
Normal file
6
apps/web/src/app/greenfood/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function GreenfoodPage() {
|
||||
redirect('/districts/greenfood')
|
||||
}
|
||||
|
||||
6
apps/web/src/app/soul/page.tsx
Normal file
6
apps/web/src/app/soul/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function SoulPage() {
|
||||
redirect('/districts/soul')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user