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