feat: scaffold rSocials-online with landing page and Postiz deployment
Next.js 16 landing page with r* ecosystem treatment: - Hero, features, platform grid, self-hosted advantages, deploy CTA - OKLCH coral/violet theme, Shadcn/ui components, Geist fonts - Dockerized with multi-stage build and Traefik labels (rsocials.online) Postiz community deployment stack: - Postiz app + PostgreSQL + Redis + Temporal workflow engine - 20+ social platforms (X, Bluesky, Mastodon, LinkedIn, Discord, etc.) - SMTP email via Mailcow (mailcow-network integration) - Security hardened (cap_drop ALL, no-new-privileges, network segmentation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
fb3d93be95
|
|
@ -0,0 +1,41 @@
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
postiz/.env
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files first for layer caching
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci || npm install
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY src ./src
|
||||||
|
COPY public ./public
|
||||||
|
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
|
||||||
|
COPY eslint.config.mjs ./
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files from builder
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Set ownership
|
||||||
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
services:
|
||||||
|
rsocials:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rsocials
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.rsocials.rule=Host(`rsocials.online`) || Host(`www.rsocials.online`)"
|
||||||
|
- "traefik.http.routers.rsocials.entrypoints=web"
|
||||||
|
- "traefik.http.services.rsocials.loadbalancer.server.port=3000"
|
||||||
|
- "traefik.docker.network=traefik-public"
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /home/nextjs/.npm
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [...compat.extends("next/core-web-vitals")];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "rsocials-online",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
|
"next": "16.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Postiz - rSocials Community Configuration
|
||||||
|
# Copy to .env and fill in values
|
||||||
|
|
||||||
|
# Database password (generate a strong random password)
|
||||||
|
POSTGRES_PASSWORD=change_me_to_random_password
|
||||||
|
|
||||||
|
# JWT Secret (generate a random string - at least 32 characters)
|
||||||
|
JWT_SECRET=change_me_to_random_secret_string_at_least_32_chars
|
||||||
|
|
||||||
|
# === Social Media API Keys (optional - add as needed) ===
|
||||||
|
|
||||||
|
# --- X (Twitter) ---
|
||||||
|
X_API_KEY=
|
||||||
|
X_API_SECRET=
|
||||||
|
|
||||||
|
# --- LinkedIn ---
|
||||||
|
LINKEDIN_CLIENT_ID=
|
||||||
|
LINKEDIN_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- Reddit ---
|
||||||
|
# Portal: https://www.reddit.com/prefs/apps
|
||||||
|
# Redirect URI: https://socials.rsocials.online/integrations/social/reddit
|
||||||
|
REDDIT_CLIENT_ID=
|
||||||
|
REDDIT_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- Meta (Facebook/Threads) ---
|
||||||
|
# Portal: https://developers.facebook.com/
|
||||||
|
FACEBOOK_APP_ID=
|
||||||
|
FACEBOOK_APP_SECRET=
|
||||||
|
THREADS_APP_ID=
|
||||||
|
THREADS_APP_SECRET=
|
||||||
|
|
||||||
|
# --- YouTube ---
|
||||||
|
YOUTUBE_CLIENT_ID=
|
||||||
|
YOUTUBE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- TikTok ---
|
||||||
|
TIKTOK_CLIENT_ID=
|
||||||
|
TIKTOK_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- Discord ---
|
||||||
|
# Portal: https://discord.com/developers/applications
|
||||||
|
# Redirect: https://socials.rsocials.online/integrations/social/discord
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_BOT_TOKEN_ID=
|
||||||
|
|
||||||
|
# --- Mastodon ---
|
||||||
|
MASTODON_URL=https://mastodon.social
|
||||||
|
MASTODON_CLIENT_ID=
|
||||||
|
MASTODON_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- Slack ---
|
||||||
|
# Portal: https://api.slack.com/apps
|
||||||
|
# Redirect: https://socials.rsocials.online/integrations/social/slack
|
||||||
|
SLACK_ID=
|
||||||
|
SLACK_SECRET=
|
||||||
|
SLACK_SIGNING_SECRET=
|
||||||
|
|
||||||
|
# --- Pinterest ---
|
||||||
|
PINTEREST_CLIENT_ID=
|
||||||
|
PINTEREST_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# === Email (SMTP via Mailcow) ===
|
||||||
|
# Password for noreply@rmail.online mailbox
|
||||||
|
EMAIL_PASS=
|
||||||
|
|
||||||
|
# === AI (optional) ===
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
services:
|
||||||
|
postiz-rsocials:
|
||||||
|
image: ghcr.io/gitroomhq/postiz-app:latest
|
||||||
|
container_name: postiz-rsocials
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MAIN_URL: 'https://socials.rsocials.online'
|
||||||
|
FRONTEND_URL: 'https://socials.rsocials.online'
|
||||||
|
NEXT_PUBLIC_BACKEND_URL: 'https://socials.rsocials.online/api'
|
||||||
|
JWT_SECRET: '${JWT_SECRET}'
|
||||||
|
DATABASE_URL: 'postgresql://postiz:${POSTGRES_PASSWORD}@postiz-rsocials-postgres:5432/postiz'
|
||||||
|
REDIS_URL: 'redis://postiz-rsocials-redis:6379'
|
||||||
|
BACKEND_INTERNAL_URL: 'http://localhost:3000'
|
||||||
|
TEMPORAL_ADDRESS: "postiz-rsocials-temporal:7233"
|
||||||
|
IS_GENERAL: 'true'
|
||||||
|
DISABLE_REGISTRATION: 'false'
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
STORAGE_PROVIDER: 'local'
|
||||||
|
UPLOAD_DIRECTORY: '/uploads'
|
||||||
|
NEXT_PUBLIC_UPLOAD_DIRECTORY: '/uploads'
|
||||||
|
|
||||||
|
# Social Media API Settings (configure in .env)
|
||||||
|
X_API_KEY: '${X_API_KEY:-}'
|
||||||
|
X_API_SECRET: '${X_API_SECRET:-}'
|
||||||
|
LINKEDIN_CLIENT_ID: '${LINKEDIN_CLIENT_ID:-}'
|
||||||
|
LINKEDIN_CLIENT_SECRET: '${LINKEDIN_CLIENT_SECRET:-}'
|
||||||
|
REDDIT_CLIENT_ID: '${REDDIT_CLIENT_ID:-}'
|
||||||
|
REDDIT_CLIENT_SECRET: '${REDDIT_CLIENT_SECRET:-}'
|
||||||
|
THREADS_APP_ID: '${THREADS_APP_ID:-}'
|
||||||
|
THREADS_APP_SECRET: '${THREADS_APP_SECRET:-}'
|
||||||
|
FACEBOOK_APP_ID: '${FACEBOOK_APP_ID:-}'
|
||||||
|
FACEBOOK_APP_SECRET: '${FACEBOOK_APP_SECRET:-}'
|
||||||
|
YOUTUBE_CLIENT_ID: '${YOUTUBE_CLIENT_ID:-}'
|
||||||
|
YOUTUBE_CLIENT_SECRET: '${YOUTUBE_CLIENT_SECRET:-}'
|
||||||
|
TIKTOK_CLIENT_ID: '${TIKTOK_CLIENT_ID:-}'
|
||||||
|
TIKTOK_CLIENT_SECRET: '${TIKTOK_CLIENT_SECRET:-}'
|
||||||
|
DISCORD_CLIENT_ID: '${DISCORD_CLIENT_ID:-}'
|
||||||
|
DISCORD_CLIENT_SECRET: '${DISCORD_CLIENT_SECRET:-}'
|
||||||
|
DISCORD_BOT_TOKEN_ID: '${DISCORD_BOT_TOKEN_ID:-}'
|
||||||
|
MASTODON_URL: '${MASTODON_URL:-https://mastodon.social}'
|
||||||
|
MASTODON_CLIENT_ID: '${MASTODON_CLIENT_ID:-}'
|
||||||
|
MASTODON_CLIENT_SECRET: '${MASTODON_CLIENT_SECRET:-}'
|
||||||
|
SLACK_ID: '${SLACK_ID:-}'
|
||||||
|
SLACK_SECRET: '${SLACK_SECRET:-}'
|
||||||
|
SLACK_SIGNING_SECRET: '${SLACK_SIGNING_SECRET:-}'
|
||||||
|
PINTEREST_CLIENT_ID: '${PINTEREST_CLIENT_ID:-}'
|
||||||
|
PINTEREST_CLIENT_SECRET: '${PINTEREST_CLIENT_SECRET:-}'
|
||||||
|
|
||||||
|
# Email (SMTP via Mailcow / mail.rmail.online)
|
||||||
|
EMAIL_PROVIDER: 'nodemailer'
|
||||||
|
EMAIL_FROM_NAME: 'rSocials'
|
||||||
|
EMAIL_FROM_ADDRESS: 'noreply@rmail.online'
|
||||||
|
EMAIL_HOST: 'mailcowdockerized-postfix-mailcow-1'
|
||||||
|
EMAIL_PORT: '587'
|
||||||
|
EMAIL_SECURE: 'false'
|
||||||
|
EMAIL_USER: 'noreply@rmail.online'
|
||||||
|
EMAIL_PASS: '${EMAIL_PASS}'
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED: '0'
|
||||||
|
|
||||||
|
# AI
|
||||||
|
OPENAI_API_KEY: '${OPENAI_API_KEY:-}'
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
NX_ADD_PLUGINS: false
|
||||||
|
API_LIMIT: 30
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- postiz-rsocials-config:/config/
|
||||||
|
- postiz-rsocials-uploads:/uploads/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.postiz-rsocials.rule=Host(`socials.rsocials.online`)"
|
||||||
|
- "traefik.http.routers.postiz-rsocials.entrypoints=web"
|
||||||
|
- "traefik.http.services.postiz-rsocials.loadbalancer.server.port=5000"
|
||||||
|
- "traefik.docker.network=traefik-public"
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
- postiz-rsocials-internal
|
||||||
|
- mailcow-network
|
||||||
|
depends_on:
|
||||||
|
postiz-rsocials-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
postiz-rsocials-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
postiz-rsocials-postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: postiz-rsocials-postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}'
|
||||||
|
POSTGRES_USER: postiz
|
||||||
|
POSTGRES_DB: postiz
|
||||||
|
volumes:
|
||||||
|
- postiz-rsocials-postgres-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- postiz-rsocials-internal
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U postiz -d postiz
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- FOWNER
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
postiz-rsocials-redis:
|
||||||
|
image: redis:7.2
|
||||||
|
container_name: postiz-rsocials-redis
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
volumes:
|
||||||
|
- postiz-rsocials-redis-data:/data
|
||||||
|
networks:
|
||||||
|
- postiz-rsocials-internal
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
# Temporal Stack (Workflow Engine for scheduling)
|
||||||
|
postiz-rsocials-temporal-postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: postiz-rsocials-temporal-postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: temporal
|
||||||
|
POSTGRES_USER: temporal
|
||||||
|
networks:
|
||||||
|
- postiz-rsocials-internal
|
||||||
|
volumes:
|
||||||
|
- postiz-rsocials-temporal-postgres-data:/var/lib/postgresql/data
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- FOWNER
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
postiz-rsocials-temporal:
|
||||||
|
image: temporalio/auto-setup:1.28.1
|
||||||
|
container_name: postiz-rsocials-temporal
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postiz-rsocials-temporal-postgres
|
||||||
|
environment:
|
||||||
|
- DB=postgres12
|
||||||
|
- DB_PORT=5432
|
||||||
|
- POSTGRES_USER=temporal
|
||||||
|
- POSTGRES_PWD=temporal
|
||||||
|
- POSTGRES_SEEDS=postiz-rsocials-temporal-postgres
|
||||||
|
- TEMPORAL_NAMESPACE=default
|
||||||
|
networks:
|
||||||
|
- postiz-rsocials-internal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postiz-rsocials-postgres-data:
|
||||||
|
postiz-rsocials-redis-data:
|
||||||
|
postiz-rsocials-config:
|
||||||
|
postiz-rsocials-uploads:
|
||||||
|
postiz-rsocials-temporal-postgres-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
postiz-rsocials-internal:
|
||||||
|
internal: true
|
||||||
|
mailcow-network:
|
||||||
|
external: true
|
||||||
|
name: mailcowdockerized_mailcow-network
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(0.98 0.005 30);
|
||||||
|
--foreground: oklch(0.145 0.02 30);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0.02 30);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0.02 30);
|
||||||
|
--primary: oklch(0.6 0.2 30);
|
||||||
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
|
--secondary: oklch(0.85 0.06 280);
|
||||||
|
--secondary-foreground: oklch(0.2 0.02 280);
|
||||||
|
--muted: oklch(0.95 0.01 30);
|
||||||
|
--muted-foreground: oklch(0.45 0.03 30);
|
||||||
|
--accent: oklch(0.55 0.18 280);
|
||||||
|
--accent-foreground: oklch(0.98 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.88 0.02 30);
|
||||||
|
--input: oklch(0.92 0.01 30);
|
||||||
|
--ring: oklch(0.6 0.2 30);
|
||||||
|
--chart-1: oklch(0.6 0.2 30);
|
||||||
|
--chart-2: oklch(0.55 0.18 280);
|
||||||
|
--chart-3: oklch(0.65 0.15 145);
|
||||||
|
--chart-4: oklch(0.75 0.15 85);
|
||||||
|
--chart-5: oklch(0.7 0.2 320);
|
||||||
|
--sidebar: oklch(0.98 0.005 30);
|
||||||
|
--sidebar-foreground: oklch(0.145 0.02 30);
|
||||||
|
--sidebar-primary: oklch(0.6 0.2 30);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-accent: oklch(0.92 0.02 30);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2 0.02 30);
|
||||||
|
--sidebar-border: oklch(0.88 0.02 30);
|
||||||
|
--sidebar-ring: oklch(0.6 0.2 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.12 0.02 30);
|
||||||
|
--foreground: oklch(0.95 0.01 30);
|
||||||
|
--card: oklch(0.18 0.02 30);
|
||||||
|
--card-foreground: oklch(0.95 0.01 30);
|
||||||
|
--popover: oklch(0.18 0.02 30);
|
||||||
|
--popover-foreground: oklch(0.95 0.01 30);
|
||||||
|
--primary: oklch(0.7 0.2 30);
|
||||||
|
--primary-foreground: oklch(0.12 0.02 30);
|
||||||
|
--secondary: oklch(0.35 0.06 280);
|
||||||
|
--secondary-foreground: oklch(0.95 0.01 280);
|
||||||
|
--muted: oklch(0.25 0.02 30);
|
||||||
|
--muted-foreground: oklch(0.65 0.03 30);
|
||||||
|
--accent: oklch(0.6 0.15 280);
|
||||||
|
--accent-foreground: oklch(0.95 0.01 280);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(0.3 0.02 30);
|
||||||
|
--input: oklch(0.25 0.02 30);
|
||||||
|
--ring: oklch(0.7 0.2 30);
|
||||||
|
--chart-1: oklch(0.7 0.2 30);
|
||||||
|
--chart-2: oklch(0.6 0.15 280);
|
||||||
|
--chart-3: oklch(0.65 0.15 145);
|
||||||
|
--chart-4: oklch(0.75 0.15 85);
|
||||||
|
--chart-5: oklch(0.7 0.2 320);
|
||||||
|
--sidebar: oklch(0.15 0.02 30);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.01 30);
|
||||||
|
--sidebar-primary: oklch(0.7 0.2 30);
|
||||||
|
--sidebar-primary-foreground: oklch(0.12 0.02 30);
|
||||||
|
--sidebar-accent: oklch(0.25 0.02 30);
|
||||||
|
--sidebar-accent-foreground: oklch(0.95 0.01 30);
|
||||||
|
--sidebar-border: oklch(0.3 0.02 30);
|
||||||
|
--sidebar-ring: oklch(0.7 0.2 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { Providers } from "@/components/Providers";
|
||||||
|
import { Navbar } from "@/components/Navbar";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "rSocials - Community Social Media Management",
|
||||||
|
description:
|
||||||
|
"Self-hosted social media scheduling and management for communities. Powered by Postiz. Schedule posts, manage multiple platforms, and collaborate with your team — all under your control.",
|
||||||
|
keywords: [
|
||||||
|
"social media",
|
||||||
|
"scheduling",
|
||||||
|
"postiz",
|
||||||
|
"self-hosted",
|
||||||
|
"community",
|
||||||
|
"management",
|
||||||
|
"rSpace",
|
||||||
|
"open source",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-background`}
|
||||||
|
>
|
||||||
|
<Providers>
|
||||||
|
<Navbar />
|
||||||
|
<main className="container mx-auto px-4 py-8">{children}</main>
|
||||||
|
<footer className="border-t border-border/50 py-8 mt-16">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground mb-4">
|
||||||
|
<span className="font-medium text-foreground/60">r* Ecosystem</span>
|
||||||
|
<a href="https://rspace.online" className="hover:text-foreground transition-colors">rSpace</a>
|
||||||
|
<a href="https://rmaps.online" className="hover:text-foreground transition-colors">rMaps</a>
|
||||||
|
<a href="https://rnotes.online" className="hover:text-foreground transition-colors">rNotes</a>
|
||||||
|
<a href="https://rvote.online" className="hover:text-foreground transition-colors">rVote</a>
|
||||||
|
<a href="https://rwork.online" className="hover:text-foreground transition-colors">rWork</a>
|
||||||
|
<a href="https://rfunds.online" className="hover:text-foreground transition-colors">rFunds</a>
|
||||||
|
<a href="https://rchats.online" className="hover:text-foreground transition-colors">rChats</a>
|
||||||
|
<a href="https://rcart.online" className="hover:text-foreground transition-colors">rCart</a>
|
||||||
|
<a href="https://rwallet.online" className="hover:text-foreground transition-colors">rWallet</a>
|
||||||
|
<a href="https://rfiles.online" className="hover:text-foreground transition-colors">rFiles</a>
|
||||||
|
<a href="https://rinbox.online" className="hover:text-foreground transition-colors">rInbox</a>
|
||||||
|
<a href="https://rnetwork.online" className="hover:text-foreground transition-colors">rNetwork</a>
|
||||||
|
<a href="https://rsocials.online" className="hover:text-foreground transition-colors font-medium text-foreground/80">rSocials</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-xs text-muted-foreground/60">
|
||||||
|
Part of the r* ecosystem — collaborative tools for communities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Calendar,
|
||||||
|
Users,
|
||||||
|
Sparkles,
|
||||||
|
Globe,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
|
BarChart3,
|
||||||
|
Share2,
|
||||||
|
Clock,
|
||||||
|
Megaphone,
|
||||||
|
Palette,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-16">
|
||||||
|
{/* Hero section */}
|
||||||
|
<section className="relative text-center py-8 sm:py-16 space-y-6 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 -z-10" />
|
||||||
|
<div className="absolute top-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl -z-10" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-96 h-96 bg-accent/5 rounded-full blur-3xl -z-10" />
|
||||||
|
|
||||||
|
<Badge variant="secondary" className="text-sm px-4 py-1 bg-primary/10 text-primary border-primary/20">
|
||||||
|
Part of the rSpace Ecosystem
|
||||||
|
</Badge>
|
||||||
|
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight max-w-4xl mx-auto leading-tight">
|
||||||
|
Social Media<br />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent">Under Your Control</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Self-hosted social media scheduling powered by <strong className="text-foreground">Postiz</strong>.
|
||||||
|
Schedule posts, manage multiple platforms, and collaborate with your
|
||||||
|
community — <strong className="text-foreground">no vendor lock-in</strong>.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center gap-4 pt-4">
|
||||||
|
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70">
|
||||||
|
<a href="https://socials.rsocials.online">
|
||||||
|
Launch Postiz
|
||||||
|
<ArrowRight className="ml-2 h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
|
||||||
|
<a href="#deploy">Deploy Your Own</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* How it works */}
|
||||||
|
<section className="py-8">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<Badge variant="secondary" className="mb-3 bg-muted text-muted-foreground">
|
||||||
|
How It Works
|
||||||
|
</Badge>
|
||||||
|
<h2 className="text-2xl font-bold">Social Media Management in 3 Steps</h2>
|
||||||
|
<p className="text-lg text-muted-foreground mt-2 max-w-2xl mx-auto">
|
||||||
|
<strong className="text-primary">Connect</strong> your social accounts,{" "}
|
||||||
|
<strong className="text-accent">schedule</strong> your content, and{" "}
|
||||||
|
<strong className="text-foreground">let Postiz handle the rest</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="border-2 border-primary/40 bg-gradient-to-br from-primary/10 to-primary/5 overflow-hidden">
|
||||||
|
<CardContent className="pt-5 pb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center">
|
||||||
|
<Share2 className="h-4 w-4 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg">1. Connect Platforms</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Link your X, Bluesky, Mastodon, LinkedIn, Discord, Reddit, YouTube,
|
||||||
|
TikTok, and more. OAuth flow — your credentials stay on your server.
|
||||||
|
<strong className="text-foreground block mt-2">20+ platforms supported.</strong>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 border-accent/40 bg-gradient-to-br from-accent/10 to-accent/5 overflow-hidden">
|
||||||
|
<CardContent className="pt-5 pb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-accent flex items-center justify-center">
|
||||||
|
<Calendar className="h-4 w-4 text-accent-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg">2. Schedule Content</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Use the calendar view to plan your content. Customize posts per platform,
|
||||||
|
use AI to generate copy, and design images with the built-in editor.
|
||||||
|
<strong className="text-foreground block mt-2">Visual calendar with drag & drop.</strong>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 border-secondary/40 bg-gradient-to-br from-secondary/10 to-secondary/5 overflow-hidden">
|
||||||
|
<CardContent className="pt-5 pb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-secondary flex items-center justify-center">
|
||||||
|
<Zap className="h-4 w-4 text-secondary-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg">3. Publish Automatically</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Postiz publishes on schedule via the Temporal workflow engine. Track
|
||||||
|
performance and iterate on what works.
|
||||||
|
<strong className="text-foreground block mt-2">Set it and forget it.</strong>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Supported Platforms */}
|
||||||
|
<section id="platforms" className="py-8">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">20+ Platforms, One Dashboard</h2>
|
||||||
|
<p className="text-muted-foreground">Post everywhere your community lives</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||||
|
{[
|
||||||
|
{ name: "X / Twitter", status: "live" },
|
||||||
|
{ name: "Bluesky", status: "live" },
|
||||||
|
{ name: "Mastodon", status: "live" },
|
||||||
|
{ name: "LinkedIn", status: "live" },
|
||||||
|
{ name: "Discord", status: "live" },
|
||||||
|
{ name: "Reddit", status: "live" },
|
||||||
|
{ name: "YouTube", status: "live" },
|
||||||
|
{ name: "TikTok", status: "live" },
|
||||||
|
{ name: "Facebook", status: "live" },
|
||||||
|
{ name: "Threads", status: "live" },
|
||||||
|
{ name: "Pinterest", status: "live" },
|
||||||
|
{ name: "Slack", status: "live" },
|
||||||
|
{ name: "Telegram", status: "live" },
|
||||||
|
{ name: "Instagram", status: "live" },
|
||||||
|
{ name: "Dribbble", status: "live" },
|
||||||
|
{ name: "RSS Auto-Post", status: "live" },
|
||||||
|
].map((platform) => (
|
||||||
|
<Card key={platform.name} className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="py-4 text-center">
|
||||||
|
<p className="font-medium text-sm">{platform.name}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<section id="features" className="py-8">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Everything Your Community Needs</h2>
|
||||||
|
<p className="text-muted-foreground">Full-featured social media management, self-hosted and open source</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Calendar className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">Calendar View</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Visual scheduling with drag & drop. Plan weeks of content at a glance with an intuitive calendar interface.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-accent to-accent/60 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Sparkles className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">AI Assistant</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Generate post ideas, write compelling copy, and optimize for engagement. Powered by your own AI key.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Palette className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">Built-in Designer</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Canva-like design tool built right in. Create professional images and graphics without leaving the app.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Users className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">Team Collaboration</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Invite team members with role-based access. Draft, review, and approve posts as a community.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Megaphone className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">Per-Platform Editing</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Customize each post per platform. Different copy, hashtags, and media for X vs LinkedIn vs Mastodon.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Clock className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">RSS Auto-Post</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Auto-publish from RSS feeds. New blog posts, podcast episodes, or news articles go out automatically.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-rose-500 to-pink-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<BarChart3 className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">Analytics</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Track post performance across all platforms. See what resonates and optimize your content strategy.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-teal-500 to-cyan-600 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Globe className="h-6 w-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-1">rSpace Ecosystem</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Part of the r* suite. Integrates with rSpace canvas, rWork project management, and rNetwork CRM.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Self-hosted advantage */}
|
||||||
|
<section className="py-8">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Why Self-Hosted?</h2>
|
||||||
|
<p className="text-muted-foreground">No subscriptions. No locked features. No data harvesting.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-transparent">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<Shield className="h-8 w-8 text-primary mb-3" />
|
||||||
|
<h3 className="font-bold text-lg mb-2">Your Data, Your Server</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
API keys, credentials, and content stay on infrastructure you control.
|
||||||
|
No third party sees your social accounts or analytics.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 border-accent/30 bg-gradient-to-br from-accent/5 to-transparent">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<Zap className="h-8 w-8 text-accent mb-3" />
|
||||||
|
<h3 className="font-bold text-lg mb-2">No Per-Seat Pricing</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Invite your entire community. Buffer and Hootsuite charge $100+/mo for teams.
|
||||||
|
Postiz is free, forever, with no feature gates.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 border-secondary/30 bg-gradient-to-br from-secondary/5 to-transparent">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<Globe className="h-8 w-8 text-secondary-foreground mb-3" />
|
||||||
|
<h3 className="font-bold text-lg mb-2">Open Source</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Full source code on GitHub. Audit it, fork it, extend it.
|
||||||
|
Community-driven development with no enterprise paywalls.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Deploy CTA */}
|
||||||
|
<section id="deploy" className="py-12">
|
||||||
|
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/10 via-accent/5 to-secondary/10 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/10 rounded-full blur-3xl" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-64 h-64 bg-accent/10 rounded-full blur-3xl" />
|
||||||
|
<CardContent className="py-12 text-center space-y-6 relative">
|
||||||
|
<Badge className="bg-primary/10 text-primary border-primary/20">Deploy for Your Community</Badge>
|
||||||
|
<h2 className="text-3xl font-bold">Get Started in Minutes</h2>
|
||||||
|
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
|
||||||
|
Clone the repo, configure your <code className="text-primary font-mono">.env</code>, and run{" "}
|
||||||
|
<code className="text-primary font-mono">docker compose up</code>. That's it.
|
||||||
|
Postiz handles scheduling, Temporal handles workflows, Traefik handles routing.
|
||||||
|
</p>
|
||||||
|
<div className="bg-card/80 backdrop-blur border rounded-lg p-4 max-w-lg mx-auto text-left font-mono text-sm">
|
||||||
|
<p className="text-muted-foreground"># Clone and deploy</p>
|
||||||
|
<p>git clone https://gitea.jeffemmett.com/jeffemmett/rsocials-online</p>
|
||||||
|
<p>cd rsocials-online/postiz</p>
|
||||||
|
<p>cp .env.example .env</p>
|
||||||
|
<p className="text-muted-foreground"># Edit .env with your API keys</p>
|
||||||
|
<p>docker compose up -d</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||||
|
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-accent hover:opacity-90">
|
||||||
|
<a href="https://socials.rsocials.online">
|
||||||
|
Try the Live Instance
|
||||||
|
<ArrowRight className="ml-2 h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
|
||||||
|
<a href="https://github.com/gitroomhq/postiz-app" target="_blank" rel="noopener noreferrer">
|
||||||
|
Postiz on GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
return (
|
||||||
|
<nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex h-16 items-center justify-between">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<Link href="/" className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold text-primary">rSocials</span>
|
||||||
|
</Link>
|
||||||
|
<div className="hidden md:flex items-center gap-4">
|
||||||
|
<Link
|
||||||
|
href="#features"
|
||||||
|
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Features
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="#platforms"
|
||||||
|
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Platforms
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="#deploy"
|
||||||
|
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" asChild>
|
||||||
|
<a href="https://github.com/gitroomhq/postiz-app" target="_blank" rel="noopener noreferrer">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<a href="https://socials.rsocials.online">
|
||||||
|
Launch Postiz
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<Toaster position="bottom-right" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue