-
Welcome to Synapsis
-
- A federated social network designed as global communication infrastructure.
- Signal over noise. Identity that's truly yours.
+ {!user && (
+
+
+ Join Synapsis to post and interact
+
+ Login or Register
+
+ )}
-
-
Node Info
-
- {process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node'}
-
-
- Running Synapsis v0.1.0
-
+ {feedType === 'curated' && feedMeta && (
+
+
Curated feed
+
+ We rank posts using recency and engagement. Following gets a boost, and your own posts stay visible.
+
+
+ Weights: engagement {feedMeta.weights.engagement}, recency {feedMeta.weights.recency}, follow boost {feedMeta.weights.followBoost}.
+
+
+ Window: {feedMeta.windowHours} hours. Seed: {feedMeta.seedLimit} posts.
+
-
-
+ )}
+
+ {loading ? (
+
+ Loading...
+
+ ) : posts.length === 0 ? (
+
+
No posts yet
+
Be the first to post something!
+
+ ) : (
+ posts.map(post => (
+
+ ))
+ )}
+ >
);
}
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
new file mode 100644
index 0000000..2e0ba44
--- /dev/null
+++ b/src/components/Icons.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+
+export const ShieldIcon = () => (
+
+);
+
+export const HomeIcon = () => (
+
+);
+
+export const SearchIcon = () => (
+
+);
+
+export const BellIcon = () => (
+
+);
+
+export const UserIcon = () => (
+
+);
+
+export const HeartIcon = ({ filled }: { filled?: boolean }) => (
+
+);
+
+export const RepeatIcon = () => (
+
+);
+
+export const MessageIcon = () => (
+
+);
+
+export const FlagIcon = () => (
+
+);
+
+export const TrendingIcon = () => (
+
+);
+
+export const UsersIcon = () => (
+
+);
+
+export const SynapsisLogo = () => (
+
+);
+
+export const ArrowLeftIcon = () => (
+
+);
+
+export const CalendarIcon = () => (
+
+);
diff --git a/src/components/LayoutWrapper.tsx b/src/components/LayoutWrapper.tsx
new file mode 100644
index 0000000..bdbeebe
--- /dev/null
+++ b/src/components/LayoutWrapper.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import { Sidebar } from './Sidebar';
+import { RightSidebar } from './RightSidebar';
+
+export function LayoutWrapper({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+
+ // Paths that should NOT have the app layout
+ const isStandalone =
+ pathname === '/login' ||
+ pathname === '/register' ||
+ pathname?.startsWith('/install') ||
+ pathname?.startsWith('/admin');
+
+ if (isStandalone) {
+ return <>{children}>;
+ }
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx
new file mode 100644
index 0000000..ac4055d
--- /dev/null
+++ b/src/components/RightSidebar.tsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+export function RightSidebar() {
+ const [nodeInfo, setNodeInfo] = useState({
+ name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
+ description: 'A federated social network designed as global communication infrastructure. Signal over noise. Identity that is truly yours.',
+ longDescription: '',
+ rules: '',
+ bannerUrl: '',
+ });
+
+ useEffect(() => {
+ fetch('/api/node')
+ .then(res => res.json())
+ .then(data => {
+ if (data.name) {
+ setNodeInfo(prev => ({ ...prev, ...data }));
+ }
+ })
+ .catch(() => { });
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
new file mode 100644
index 0000000..1233a63
--- /dev/null
+++ b/src/components/Sidebar.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useAuth } from '@/lib/contexts/AuthContext';
+import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SynapsisLogo } from './Icons';
+
+export function Sidebar() {
+ const { user, isAdmin } = useAuth();
+ const pathname = usePathname();
+
+ // Home is exact match
+ const isHome = pathname === '/';
+
+ return (
+
+ );
+}
diff --git a/src/db/schema.ts b/src/db/schema.ts
index 6d3e6f4..a5115ae 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -10,6 +10,9 @@ export const nodes = pgTable('nodes', {
domain: text('domain').notNull().unique(),
name: text('name').notNull(),
description: text('description'),
+ longDescription: text('long_description'),
+ rules: text('rules'),
+ bannerUrl: text('banner_url'),
accentColor: text('accent_color').default('#FFFFFF'),
publicKey: text('public_key'),
createdAt: timestamp('created_at').defaultNow().notNull(),
diff --git a/src/lib/contexts/AuthContext.tsx b/src/lib/contexts/AuthContext.tsx
new file mode 100644
index 0000000..1866a15
--- /dev/null
+++ b/src/lib/contexts/AuthContext.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState } from 'react';
+
+export interface User {
+ id: string;
+ handle: string;
+ displayName: string;
+ avatarUrl?: string;
+}
+
+interface AuthContextType {
+ user: User | null;
+ isAdmin: boolean;
+ loading: boolean;
+ checkAdmin: () => Promise
;
+}
+
+const AuthContext = createContext({
+ user: null,
+ isAdmin: false,
+ loading: true,
+ checkAdmin: async () => { },
+});
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ const checkAdmin = async () => {
+ try {
+ const res = await fetch('/api/admin/me');
+ const data = await res.json();
+ setIsAdmin(!!data.isAdmin);
+ } catch {
+ setIsAdmin(false);
+ }
+ };
+
+ useEffect(() => {
+ const loadAuth = async () => {
+ setLoading(true);
+ try {
+ const res = await fetch('/api/auth/me');
+ if (res.ok) {
+ const data = await res.json();
+ setUser(data.user);
+ if (data.user) {
+ await checkAdmin();
+ }
+ } else {
+ setUser(null);
+ }
+ } catch {
+ setUser(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadAuth();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useAuth = () => useContext(AuthContext);