├── backend └── app │ ├── __init__.py │ ├── routers │ ├── __init__.py │ ├── __pycache__ │ │ ├── auth.cpython-311.pyc │ │ ├── content.cpython-311.pyc │ │ ├── posts.cpython-311.pyc │ │ ├── users.cpython-311.pyc │ │ ├── __init__.cpython-311.pyc │ │ ├── preferences.cpython-311.pyc │ │ └── social_accounts.cpython-311.pyc │ ├── users.py │ ├── preferences.py │ ├── auth.py │ ├── social_accounts.py │ ├── content.py │ └── posts.py │ ├── __pycache__ │ ├── auth.cpython-311.pyc │ ├── main.cpython-311.pyc │ ├── config.cpython-311.pyc │ ├── models.cpython-311.pyc │ ├── schemas.cpython-311.pyc │ ├── __init__.cpython-311.pyc │ └── database.cpython-311.pyc │ ├── database.py │ ├── main.py │ ├── config.py │ ├── schemas.py │ ├── auth.py │ ├── models.py │ └── social_media_integrations.py ├── start_backend.sh ├── frontend ├── postcss.config.js ├── tailwind.config.js ├── src │ ├── main.jsx │ ├── index.css │ ├── components │ │ ├── PrivateRoute.jsx │ │ ├── Sidebar.jsx │ │ └── Navbar.jsx │ ├── App.css │ ├── services │ │ └── api.js │ ├── context │ │ └── AuthContext.jsx │ ├── App.jsx │ ├── pages │ │ ├── Login.jsx │ │ ├── Calendar.jsx │ │ ├── Register.jsx │ │ ├── Posts.jsx │ │ ├── GenerateContent.jsx │ │ ├── Preferences.jsx │ │ └── Dashboard.jsx │ └── assets │ │ └── react.svg ├── vite.config.js ├── .gitignore ├── index.html ├── eslint.config.js ├── package.json ├── README.md └── public │ └── vite.svg ├── start_app.sh ├── .gitignore └── requirements.txt /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /start_backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd backend 3 | uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload 4 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /backend/app/__pycache__/auth.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/auth.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/main.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/main.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/config.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/config.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/schemas.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/schemas.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/database.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/__pycache__/database.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/auth.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/auth.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/content.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/content.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/posts.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/posts.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/users.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/users.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/preferences.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/preferences.cpython-311.pyc -------------------------------------------------------------------------------- /backend/app/routers/__pycache__/social_accounts.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alqudimi/SocialSparkAI/HEAD/backend/app/routers/__pycache__/social_accounts.cpython-311.pyc -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | host: '0.0.0.0', 8 | port: 5000, 9 | strictPort: true, 10 | allowedHosts: true, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /start_app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting backend server on port 8000..." 4 | cd backend 5 | uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload & 6 | BACKEND_PID=$! 7 | 8 | cd .. 9 | 10 | echo "Starting frontend server on port 5000..." 11 | cd frontend 12 | npm run dev 13 | 14 | kill $BACKEND_PID 2>/dev/null 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | frontend 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/app/routers/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from sqlalchemy.orm import Session 3 | from ..database import get_db 4 | from ..models import User 5 | from ..schemas import UserResponse 6 | from ..auth import get_current_active_user 7 | 8 | router = APIRouter(prefix="/users", tags=["users"]) 9 | 10 | @router.get("/me", response_model=UserResponse) 11 | async def read_users_me(current_user: User = Depends(get_current_active_user)): 12 | return current_user 13 | -------------------------------------------------------------------------------- /backend/app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | from .config import settings 5 | 6 | engine = create_engine(settings.database_url) 7 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 8 | 9 | Base = declarative_base() 10 | 11 | def get_db(): 12 | db = SessionLocal() 13 | try: 14 | yield db 15 | finally: 16 | db.close() 17 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | import { useAuth } from '../context/AuthContext'; 3 | 4 | export default function PrivateRoute({ children }) { 5 | const { user, loading } = useAuth(); 6 | 7 | if (loading) { 8 | return ( 9 |
10 |
11 |
12 | ); 13 | } 14 | 15 | return user ? children : ; 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.so 5 | .Python 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | downloads/ 10 | eggs/ 11 | .eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | wheels/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | MANIFEST 22 | venv/ 23 | ENV/ 24 | env/ 25 | .venv 26 | 27 | node_modules/ 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | .DS_Store 33 | *.local 34 | dist-ssr 35 | *.tsbuildinfo 36 | 37 | .env 38 | .env.local 39 | .env.*.local 40 | *.db 41 | *.sqlite 42 | *.sqlite3 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.0 2 | uvicorn[standard]==0.32.0 3 | sqlalchemy==2.0.36 4 | pydantic-settings==2.6.1 5 | pydantic[email]==2.10.3 6 | python-jose[cryptography]==3.3.0 7 | passlib[bcrypt]==1.7.4 8 | python-multipart==0.0.20 9 | psycopg2-binary==2.9.10 10 | google-generativeai==0.8.3 11 | fastapi==0.115.0 12 | google-generativeai==0.8.3 13 | passlib[bcrypt]==1.7.4 14 | psycopg2-binary==2.9.10 15 | pydantic[email]==2.10.3 16 | pydantic-settings==2.6.1 17 | python-jose[cryptography]==3.3.0 18 | python-multipart==0.0.20 19 | sqlalchemy==2.0.36 20 | uvicorn[standard]==0.32.0 21 | requests 22 | requests-oauthlib 23 | tweepy 24 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs['recommended-latest'], 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/postcss": "^4.1.14", 14 | "axios": "^1.12.2", 15 | "lucide-react": "^0.544.0", 16 | "react": "^19.1.1", 17 | "react-calendar": "^6.0.0", 18 | "react-dom": "^19.1.1", 19 | "react-router-dom": "^7.9.3" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.36.0", 23 | "@types/react": "^19.1.16", 24 | "@types/react-dom": "^19.1.9", 25 | "@vitejs/plugin-react": "^5.0.4", 26 | "autoprefixer": "^10.4.21", 27 | "eslint": "^9.36.0", 28 | "eslint-plugin-react-hooks": "^5.2.0", 29 | "eslint-plugin-react-refresh": "^0.4.22", 30 | "globals": "^16.4.0", 31 | "postcss": "^8.5.6", 32 | "tailwindcss": "^4.1.14", 33 | "vite": "^7.1.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from .database import engine, Base 4 | from .routers import auth, users, social_accounts, preferences, content, posts 5 | from .config import settings 6 | 7 | Base.metadata.create_all(bind=engine) 8 | 9 | app = FastAPI(title="Smart Social Media Assistant API") 10 | 11 | app.add_middleware( 12 | CORSMiddleware, 13 | allow_origins=["*"], 14 | allow_credentials=True, 15 | allow_methods=["*"], 16 | allow_headers=["*"], 17 | ) 18 | 19 | app.include_router(auth.router, prefix="/api") 20 | app.include_router(users.router, prefix="/api") 21 | app.include_router(social_accounts.router, prefix="/api") 22 | app.include_router(preferences.router, prefix="/api") 23 | app.include_router(content.router, prefix="/api") 24 | app.include_router(posts.router, prefix="/api") 25 | 26 | @app.on_event("startup") 27 | async def startup_event(): 28 | pass 29 | 30 | @app.get("/") 31 | def read_root(): 32 | return {"message": "Smart Social Media Assistant API", "status": "running"} 33 | 34 | @app.get("/health") 35 | def health_check(): 36 | return {"status": "healthy"} 37 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## React Compiler 11 | 12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 | 14 | ## Expanding the ESLint configuration 15 | 16 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. 17 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useLocation } from 'react-router-dom'; 2 | import { Home, Settings, FileText, Calendar, Sparkles } from 'lucide-react'; 3 | 4 | export default function Sidebar() { 5 | const location = useLocation(); 6 | 7 | const menuItems = [ 8 | { path: '/dashboard', icon: Home, label: 'Dashboard' }, 9 | { path: '/preferences', icon: Settings, label: 'Preferences' }, 10 | { path: '/generate', icon: Sparkles, label: 'Generate Content' }, 11 | { path: '/posts', icon: FileText, label: 'My Posts' }, 12 | { path: '/calendar', icon: Calendar, label: 'Calendar' }, 13 | ]; 14 | 15 | return ( 16 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from 'react-router-dom'; 2 | import { useAuth } from '../context/AuthContext'; 3 | import { LogOut, User } from 'lucide-react'; 4 | 5 | export default function Navbar() { 6 | const { user, logout } = useAuth(); 7 | const navigate = useNavigate(); 8 | 9 | const handleLogout = () => { 10 | logout(); 11 | navigate('/login'); 12 | }; 13 | 14 | return ( 15 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'; 4 | 5 | const api = axios.create({ 6 | baseURL: API_BASE_URL, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | }); 11 | 12 | api.interceptors.request.use( 13 | (config) => { 14 | const token = localStorage.getItem('token'); 15 | if (token) { 16 | config.headers.Authorization = `Bearer ${token}`; 17 | } 18 | return config; 19 | }, 20 | (error) => { 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | export const authAPI = { 26 | register: (data) => api.post('/auth/register', data), 27 | login: (data) => api.post('/auth/login', data), 28 | getCurrentUser: () => api.get('/users/me'), 29 | }; 30 | 31 | export const socialAccountsAPI = { 32 | getAll: () => api.get('/social-accounts'), 33 | create: (data) => api.post('/social-accounts', data), 34 | delete: (id) => api.delete(`/social-accounts/${id}`), 35 | toggle: (id) => api.patch(`/social-accounts/${id}/toggle`), 36 | }; 37 | 38 | export const preferencesAPI = { 39 | get: () => api.get('/preferences'), 40 | update: (data) => api.put('/preferences', data), 41 | }; 42 | 43 | export const contentAPI = { 44 | generate: (data) => api.post('/content/generate', data), 45 | }; 46 | 47 | export const postsAPI = { 48 | getAll: () => api.get('/posts'), 49 | get: (id) => api.get(`/posts/${id}`), 50 | create: (data) => api.post('/posts', data), 51 | update: (id, data) => api.patch(`/posts/${id}`, data), 52 | delete: (id) => api.delete(`/posts/${id}`), 53 | publish: (id) => api.post(`/posts/${id}/publish`), 54 | }; 55 | 56 | export default api; 57 | -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useContext, useEffect } from 'react'; 2 | import { authAPI } from '../services/api'; 3 | 4 | const AuthContext = createContext(null); 5 | 6 | export const AuthProvider = ({ children }) => { 7 | const [user, setUser] = useState(null); 8 | const [loading, setLoading] = useState(true); 9 | 10 | useEffect(() => { 11 | const token = localStorage.getItem('token'); 12 | if (token) { 13 | loadUser(); 14 | } else { 15 | setLoading(false); 16 | } 17 | }, []); 18 | 19 | const loadUser = async () => { 20 | try { 21 | const response = await authAPI.getCurrentUser(); 22 | setUser(response.data); 23 | } catch (error) { 24 | localStorage.removeItem('token'); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | const login = async (email, password) => { 31 | const response = await authAPI.login({ email, password }); 32 | localStorage.setItem('token', response.data.access_token); 33 | await loadUser(); 34 | return response.data; 35 | }; 36 | 37 | const register = async (email, username, password) => { 38 | const response = await authAPI.register({ email, username, password }); 39 | return response.data; 40 | }; 41 | 42 | const logout = () => { 43 | localStorage.removeItem('token'); 44 | setUser(null); 45 | }; 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | export const useAuth = () => { 55 | const context = useContext(AuthContext); 56 | if (!context) { 57 | throw new Error('useAuth must be used within an AuthProvider'); 58 | } 59 | return context; 60 | }; 61 | -------------------------------------------------------------------------------- /backend/app/routers/preferences.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from sqlalchemy.orm import Session 3 | from ..database import get_db 4 | from ..models import User, ContentPreference 5 | from ..schemas import ContentPreferenceCreate, ContentPreferenceResponse 6 | from ..auth import get_current_active_user 7 | 8 | router = APIRouter(prefix="/preferences", tags=["content preferences"]) 9 | 10 | @router.get("/", response_model=ContentPreferenceResponse) 11 | async def get_content_preferences( 12 | current_user: User = Depends(get_current_active_user), 13 | db: Session = Depends(get_db) 14 | ): 15 | preferences = db.query(ContentPreference).filter( 16 | ContentPreference.user_id == current_user.id 17 | ).first() 18 | 19 | if not preferences: 20 | raise HTTPException(status_code=404, detail="Content preferences not found") 21 | 22 | return preferences 23 | 24 | @router.put("/", response_model=ContentPreferenceResponse) 25 | async def update_content_preferences( 26 | preferences_data: ContentPreferenceCreate, 27 | current_user: User = Depends(get_current_active_user), 28 | db: Session = Depends(get_db) 29 | ): 30 | preferences = db.query(ContentPreference).filter( 31 | ContentPreference.user_id == current_user.id 32 | ).first() 33 | 34 | if not preferences: 35 | preferences = ContentPreference(user_id=current_user.id) 36 | db.add(preferences) 37 | 38 | preferences.topics = preferences_data.topics 39 | preferences.hashtags = preferences_data.hashtags 40 | preferences.posting_style = preferences_data.posting_style 41 | preferences.tone = preferences_data.tone 42 | preferences.content_length = preferences_data.content_length 43 | preferences.include_emojis = preferences_data.include_emojis 44 | 45 | db.commit() 46 | db.refresh(preferences) 47 | return preferences 48 | -------------------------------------------------------------------------------- /backend/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pydantic_settings import BaseSettings 3 | from pydantic import Field 4 | 5 | class Settings(BaseSettings): 6 | database_url: str = Field(default_factory=lambda: os.getenv("DATABASE_URL", "")) 7 | secret_key: str = Field(default_factory=lambda: os.getenv("SESSION_SECRET", "your-secret-key-change-in-production")) 8 | algorithm: str = "HS256" 9 | access_token_expire_minutes: int = 60 * 24 * 7 10 | gemini_api_key: str = Field(default_factory=lambda: os.getenv("GEMINI_API_KEY", "")) 11 | 12 | # X/Twitter API credentials 13 | x_api_key: str = Field(default_factory=lambda: os.getenv("X_API_KEY", "")) 14 | x_api_secret: str = Field(default_factory=lambda: os.getenv("X_API_SECRET", "")) 15 | x_bearer_token: str = Field(default_factory=lambda: os.getenv("X_BEARER_TOKEN", "")) 16 | x_access_token: str = Field(default_factory=lambda: os.getenv("X_ACCESS_TOKEN", "")) 17 | x_access_token_secret: str = Field(default_factory=lambda: os.getenv("X_ACCESS_TOKEN_SECRET", "")) 18 | 19 | # Threads API credentials 20 | threads_app_id: str = Field(default_factory=lambda: os.getenv("THREADS_APP_ID", "")) 21 | threads_app_secret: str = Field(default_factory=lambda: os.getenv("THREADS_APP_SECRET", "")) 22 | 23 | # Instagram API credentials 24 | instagram_app_id: str = Field(default_factory=lambda: os.getenv("INSTAGRAM_APP_ID", "")) 25 | instagram_app_secret: str = Field(default_factory=lambda: os.getenv("INSTAGRAM_APP_SECRET", "")) 26 | 27 | class Config: 28 | env_file = ".env" 29 | case_sensitive = False 30 | 31 | def validate_config(self): 32 | if not self.database_url: 33 | raise ValueError("DATABASE_URL environment variable is required") 34 | if self.secret_key == "your-secret-key-change-in-production": 35 | raise ValueError("SESSION_SECRET environment variable should be set for production") 36 | 37 | settings = Settings() 38 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; 2 | import { AuthProvider } from './context/AuthContext'; 3 | import PrivateRoute from './components/PrivateRoute'; 4 | import Login from './pages/Login'; 5 | import Register from './pages/Register'; 6 | import Dashboard from './pages/Dashboard'; 7 | import Preferences from './pages/Preferences'; 8 | import GenerateContent from './pages/GenerateContent'; 9 | import Posts from './pages/Posts'; 10 | import Calendar from './pages/Calendar'; 11 | 12 | function App() { 13 | return ( 14 | 15 | 16 | 17 | } /> 18 | } /> 19 | 23 | 24 | 25 | } 26 | /> 27 | 31 | 32 | 33 | } 34 | /> 35 | 39 | 40 | 41 | } 42 | /> 43 | 47 | 48 | 49 | } 50 | /> 51 | 55 | 56 | 57 | } 58 | /> 59 | } /> 60 | 61 | 62 | 63 | ); 64 | } 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /backend/app/routers/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, status 2 | from sqlalchemy.orm import Session 3 | from datetime import timedelta 4 | from ..database import get_db 5 | from ..models import User, ContentPreference 6 | from ..schemas import UserCreate, UserLogin, UserResponse, Token 7 | from ..auth import get_password_hash, authenticate_user, create_access_token 8 | from ..config import settings 9 | 10 | router = APIRouter(prefix="/auth", tags=["authentication"]) 11 | 12 | @router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED) 13 | def register(user: UserCreate, db: Session = Depends(get_db)): 14 | db_user = db.query(User).filter(User.email == user.email).first() 15 | if db_user: 16 | raise HTTPException(status_code=400, detail="Email already registered") 17 | 18 | db_user = db.query(User).filter(User.username == user.username).first() 19 | if db_user: 20 | raise HTTPException(status_code=400, detail="Username already taken") 21 | 22 | hashed_password = get_password_hash(user.password) 23 | new_user = User( 24 | email=user.email, 25 | username=user.username, 26 | hashed_password=hashed_password 27 | ) 28 | db.add(new_user) 29 | db.commit() 30 | db.refresh(new_user) 31 | 32 | content_pref = ContentPreference(user_id=new_user.id) 33 | db.add(content_pref) 34 | db.commit() 35 | 36 | return new_user 37 | 38 | @router.post("/login", response_model=Token) 39 | def login(user_credentials: UserLogin, db: Session = Depends(get_db)): 40 | user = authenticate_user(db, user_credentials.email, user_credentials.password) 41 | if not user: 42 | raise HTTPException( 43 | status_code=status.HTTP_401_UNAUTHORIZED, 44 | detail="Incorrect email or password", 45 | headers={"WWW-Authenticate": "Bearer"}, 46 | ) 47 | 48 | access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) 49 | access_token = create_access_token( 50 | data={"sub": user.email}, expires_delta=access_token_expires 51 | ) 52 | return {"access_token": access_token, "token_type": "bearer"} 53 | -------------------------------------------------------------------------------- /backend/app/routers/social_accounts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from sqlalchemy.orm import Session 3 | from typing import List 4 | from ..database import get_db 5 | from ..models import User, SocialAccount 6 | from ..schemas import SocialAccountCreate, SocialAccountResponse 7 | from ..auth import get_current_active_user 8 | 9 | router = APIRouter(prefix="/social-accounts", tags=["social accounts"]) 10 | 11 | @router.get("/", response_model=List[SocialAccountResponse]) 12 | async def get_social_accounts( 13 | current_user: User = Depends(get_current_active_user), 14 | db: Session = Depends(get_db) 15 | ): 16 | accounts = db.query(SocialAccount).filter(SocialAccount.user_id == current_user.id).all() 17 | return accounts 18 | 19 | @router.post("/", response_model=SocialAccountResponse) 20 | async def create_social_account( 21 | account: SocialAccountCreate, 22 | current_user: User = Depends(get_current_active_user), 23 | db: Session = Depends(get_db) 24 | ): 25 | new_account = SocialAccount( 26 | user_id=current_user.id, 27 | platform=account.platform, 28 | account_name=account.account_name 29 | ) 30 | db.add(new_account) 31 | db.commit() 32 | db.refresh(new_account) 33 | return new_account 34 | 35 | @router.delete("/{account_id}") 36 | async def delete_social_account( 37 | account_id: int, 38 | current_user: User = Depends(get_current_active_user), 39 | db: Session = Depends(get_db) 40 | ): 41 | account = db.query(SocialAccount).filter( 42 | SocialAccount.id == account_id, 43 | SocialAccount.user_id == current_user.id 44 | ).first() 45 | 46 | if not account: 47 | raise HTTPException(status_code=404, detail="Social account not found") 48 | 49 | db.delete(account) 50 | db.commit() 51 | return {"message": "Social account deleted successfully"} 52 | 53 | @router.patch("/{account_id}/toggle") 54 | async def toggle_social_account( 55 | account_id: int, 56 | current_user: User = Depends(get_current_active_user), 57 | db: Session = Depends(get_db) 58 | ): 59 | account = db.query(SocialAccount).filter( 60 | SocialAccount.id == account_id, 61 | SocialAccount.user_id == current_user.id 62 | ).first() 63 | 64 | if not account: 65 | raise HTTPException(status_code=404, detail="Social account not found") 66 | 67 | account.is_connected = not account.is_connected 68 | db.commit() 69 | db.refresh(account) 70 | return account 71 | -------------------------------------------------------------------------------- /backend/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, Field 2 | from typing import Optional, List 3 | from datetime import datetime 4 | 5 | class UserCreate(BaseModel): 6 | email: EmailStr 7 | username: str 8 | password: str 9 | 10 | class UserLogin(BaseModel): 11 | email: EmailStr 12 | password: str 13 | 14 | class UserResponse(BaseModel): 15 | id: int 16 | email: str 17 | username: str 18 | created_at: datetime 19 | is_active: bool 20 | 21 | class Config: 22 | from_attributes = True 23 | 24 | class Token(BaseModel): 25 | access_token: str 26 | token_type: str 27 | 28 | class TokenData(BaseModel): 29 | email: Optional[str] = None 30 | 31 | class SocialAccountCreate(BaseModel): 32 | platform: str 33 | account_name: str 34 | 35 | class SocialAccountResponse(BaseModel): 36 | id: int 37 | platform: str 38 | account_name: str 39 | is_connected: bool 40 | created_at: datetime 41 | 42 | class Config: 43 | from_attributes = True 44 | 45 | class ContentPreferenceCreate(BaseModel): 46 | topics: List[str] = [] 47 | hashtags: List[str] = [] 48 | posting_style: str = "professional" 49 | tone: str = "friendly" 50 | content_length: str = "medium" 51 | include_emojis: bool = True 52 | 53 | class ContentPreferenceResponse(BaseModel): 54 | id: int 55 | topics: List[str] 56 | hashtags: List[str] 57 | posting_style: str 58 | tone: str 59 | content_length: str 60 | include_emojis: bool 61 | 62 | class Config: 63 | from_attributes = True 64 | 65 | class GenerateContentRequest(BaseModel): 66 | topic: Optional[str] = None 67 | platform: Optional[str] = None 68 | custom_prompt: Optional[str] = None 69 | 70 | class GenerateContentResponse(BaseModel): 71 | content: str 72 | hashtags: List[str] 73 | generated_at: datetime 74 | 75 | class PostCreate(BaseModel): 76 | content: str 77 | hashtags: List[str] = [] 78 | platforms: List[str] = [] 79 | scheduled_time: Optional[datetime] = None 80 | 81 | class PostUpdate(BaseModel): 82 | content: Optional[str] = None 83 | hashtags: Optional[List[str]] = None 84 | platforms: Optional[List[str]] = None 85 | scheduled_time: Optional[datetime] = None 86 | status: Optional[str] = None 87 | 88 | class PostResponse(BaseModel): 89 | id: int 90 | content: str 91 | hashtags: List[str] 92 | platforms: List[str] 93 | scheduled_time: Optional[datetime] 94 | status: str 95 | is_published: bool 96 | created_at: datetime 97 | 98 | class Config: 99 | from_attributes = True 100 | -------------------------------------------------------------------------------- /backend/app/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | import hashlib 4 | import os 5 | from jose import JWTError, jwt 6 | from fastapi import Depends, HTTPException, status 7 | from fastapi.security import OAuth2PasswordBearer 8 | from sqlalchemy.orm import Session 9 | from .config import settings 10 | from .database import get_db 11 | from .models import User 12 | from .schemas import TokenData 13 | 14 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") 15 | 16 | def verify_password(plain_password: str, hashed_password: str) -> bool: 17 | salt = hashed_password[:64] 18 | stored_hash = hashed_password[64:] 19 | pwd_hash = hashlib.pbkdf2_hmac('sha256', plain_password.encode('utf-8'), bytes.fromhex(salt), 100000) 20 | return pwd_hash.hex() == stored_hash 21 | 22 | def get_password_hash(password: str) -> str: 23 | salt = os.urandom(32) 24 | pwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) 25 | return salt.hex() + pwd_hash.hex() 26 | 27 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 28 | to_encode = data.copy() 29 | if expires_delta: 30 | expire = datetime.utcnow() + expires_delta 31 | else: 32 | expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) 33 | to_encode.update({"exp": expire}) 34 | encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) 35 | return encoded_jwt 36 | 37 | def get_user_by_email(db: Session, email: str): 38 | return db.query(User).filter(User.email == email).first() 39 | 40 | def authenticate_user(db: Session, email: str, password: str): 41 | user = get_user_by_email(db, email) 42 | if not user: 43 | return False 44 | if not verify_password(password, user.hashed_password): 45 | return False 46 | return user 47 | 48 | async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): 49 | credentials_exception = HTTPException( 50 | status_code=status.HTTP_401_UNAUTHORIZED, 51 | detail="Could not validate credentials", 52 | headers={"WWW-Authenticate": "Bearer"}, 53 | ) 54 | try: 55 | payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) 56 | email: str = payload.get("sub") 57 | if email is None: 58 | raise credentials_exception 59 | token_data = TokenData(email=email) 60 | except JWTError: 61 | raise credentials_exception 62 | user = get_user_by_email(db, email=token_data.email) 63 | if user is None: 64 | raise credentials_exception 65 | return user 66 | 67 | async def get_current_active_user(current_user: User = Depends(get_current_user)): 68 | if not current_user.is_active: 69 | raise HTTPException(status_code=400, detail="Inactive user") 70 | return current_user 71 | -------------------------------------------------------------------------------- /backend/app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON 2 | from sqlalchemy.orm import relationship 3 | from datetime import datetime 4 | from .database import Base 5 | 6 | class User(Base): 7 | __tablename__ = "users" 8 | 9 | id = Column(Integer, primary_key=True, index=True) 10 | email = Column(String, unique=True, index=True, nullable=False) 11 | username = Column(String, unique=True, index=True, nullable=False) 12 | hashed_password = Column(String, nullable=False) 13 | created_at = Column(DateTime, default=datetime.utcnow) 14 | is_active = Column(Boolean, default=True) 15 | 16 | social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan") 17 | content_preferences = relationship("ContentPreference", back_populates="user", cascade="all, delete-orphan", uselist=False) 18 | posts = relationship("Post", back_populates="user", cascade="all, delete-orphan") 19 | 20 | class SocialAccount(Base): 21 | __tablename__ = "social_accounts" 22 | 23 | id = Column(Integer, primary_key=True, index=True) 24 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False) 25 | platform = Column(String, nullable=False) 26 | account_name = Column(String, nullable=False) 27 | is_connected = Column(Boolean, default=True) 28 | access_token = Column(String, nullable=True) 29 | created_at = Column(DateTime, default=datetime.utcnow) 30 | 31 | user = relationship("User", back_populates="social_accounts") 32 | 33 | class ContentPreference(Base): 34 | __tablename__ = "content_preferences" 35 | 36 | id = Column(Integer, primary_key=True, index=True) 37 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) 38 | topics = Column(JSON, default=list) 39 | hashtags = Column(JSON, default=list) 40 | posting_style = Column(String, default="professional") 41 | tone = Column(String, default="friendly") 42 | content_length = Column(String, default="medium") 43 | include_emojis = Column(Boolean, default=True) 44 | created_at = Column(DateTime, default=datetime.utcnow) 45 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 46 | 47 | user = relationship("User", back_populates="content_preferences") 48 | 49 | class Post(Base): 50 | __tablename__ = "posts" 51 | 52 | id = Column(Integer, primary_key=True, index=True) 53 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False) 54 | content = Column(Text, nullable=False) 55 | hashtags = Column(JSON, default=list) 56 | platforms = Column(JSON, default=list) 57 | scheduled_time = Column(DateTime, nullable=True) 58 | status = Column(String, default="draft") 59 | is_published = Column(Boolean, default=False) 60 | created_at = Column(DateTime, default=datetime.utcnow) 61 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 62 | 63 | user = relationship("User", back_populates="posts") 64 | -------------------------------------------------------------------------------- /backend/app/routers/content.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from sqlalchemy.orm import Session 3 | from datetime import datetime 4 | import google.generativeai as genai 5 | from ..database import get_db 6 | from ..models import User, ContentPreference 7 | from ..schemas import GenerateContentRequest, GenerateContentResponse 8 | from ..auth import get_current_active_user 9 | from ..config import settings 10 | 11 | router = APIRouter(prefix="/content", tags=["content generation"]) 12 | 13 | def initialize_gemini(): 14 | if settings.gemini_api_key: 15 | genai.configure(api_key=settings.gemini_api_key) 16 | return genai.GenerativeModel('gemini-2.5-flash') 17 | return None 18 | 19 | @router.post("/generate", response_model=GenerateContentResponse) 20 | async def generate_content( 21 | request: GenerateContentRequest, 22 | current_user: User = Depends(get_current_active_user), 23 | db: Session = Depends(get_db) 24 | ): 25 | preferences = db.query(ContentPreference).filter( 26 | ContentPreference.user_id == current_user.id 27 | ).first() 28 | 29 | if not preferences: 30 | raise HTTPException(status_code=404, detail="Please set your content preferences first") 31 | 32 | model = initialize_gemini() 33 | if not model: 34 | raise HTTPException(status_code=500, detail="Gemini API key not configured. Please add GEMINI_API_KEY to your environment.") 35 | 36 | topic = request.topic or (preferences.topics[0] if preferences.topics else "general topic") 37 | platform = request.platform or "social media" 38 | 39 | length_instructions = { 40 | "short": "Keep it under 100 characters", 41 | "medium": "Keep it between 100-200 characters", 42 | "long": "Keep it between 200-280 characters" 43 | } 44 | 45 | prompt = f"""Generate a {preferences.posting_style} {platform} post about {topic}. 46 | 47 | Style Guidelines: 48 | - Tone: {preferences.tone} 49 | - Length: {length_instructions.get(preferences.content_length, 'medium length')} 50 | - {'Include relevant emojis' if preferences.include_emojis else 'No emojis'} 51 | - Posting style: {preferences.posting_style} 52 | 53 | Additional requirements: 54 | - Make it engaging and authentic 55 | - Do not include hashtags in the main content 56 | - Focus on providing value to the audience 57 | 58 | Generate only the post content without any preamble or explanation.""" 59 | 60 | if request.custom_prompt: 61 | prompt = request.custom_prompt 62 | 63 | try: 64 | response = model.generate_content(prompt) 65 | content = response.text.strip() 66 | 67 | hashtag_prompt = f"Generate 3-5 relevant hashtags for this {platform} post about {topic}. Return only the hashtags separated by spaces, starting with #." 68 | hashtag_response = model.generate_content(hashtag_prompt) 69 | hashtags_text = hashtag_response.text.strip() 70 | hashtags = [tag.strip() for tag in hashtags_text.split() if tag.startswith('#')] 71 | 72 | if not hashtags and preferences.hashtags: 73 | hashtags = [f"#{tag}" if not tag.startswith('#') else tag for tag in preferences.hashtags[:3]] 74 | 75 | return GenerateContentResponse( 76 | content=content, 77 | hashtags=hashtags, 78 | generated_at=datetime.utcnow() 79 | ) 80 | 81 | except Exception as e: 82 | raise HTTPException(status_code=500, detail=f"Error generating content: {str(e)}") 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useAuth } from '../context/AuthContext'; 4 | import { LogIn } from 'lucide-react'; 5 | 6 | export default function Login() { 7 | const [email, setEmail] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const [error, setError] = useState(''); 10 | const [loading, setLoading] = useState(false); 11 | const { login } = useAuth(); 12 | const navigate = useNavigate(); 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault(); 16 | setError(''); 17 | setLoading(true); 18 | 19 | try { 20 | await login(email, password); 21 | navigate('/dashboard'); 22 | } catch (err) { 23 | setError(err.response?.data?.detail || 'Login failed. Please try again.'); 24 | } finally { 25 | setLoading(false); 26 | } 27 | }; 28 | 29 | return ( 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |

Welcome Back

39 |

Sign in to your account

40 |
41 | 42 | {error && ( 43 |
44 | {error} 45 |
46 | )} 47 | 48 |
49 |
50 | 53 | setEmail(e.target.value)} 58 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 59 | placeholder="your@email.com" 60 | /> 61 |
62 | 63 |
64 | 67 | setPassword(e.target.value)} 72 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 73 | placeholder="••••••••" 74 | /> 75 |
76 | 77 | 84 |
85 | 86 |

87 | Don't have an account?{' '} 88 | 89 | Sign up 90 | 91 |

92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/pages/Calendar.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { postsAPI } from '../services/api'; 3 | import Navbar from '../components/Navbar'; 4 | import Sidebar from '../components/Sidebar'; 5 | import { Calendar as CalendarIcon } from 'lucide-react'; 6 | import CalendarComponent from 'react-calendar'; 7 | import 'react-calendar/dist/Calendar.css'; 8 | 9 | export default function Calendar() { 10 | const [posts, setPosts] = useState([]); 11 | const [selectedDate, setSelectedDate] = useState(new Date()); 12 | const [loading, setLoading] = useState(true); 13 | 14 | useEffect(() => { 15 | loadPosts(); 16 | }, []); 17 | 18 | const loadPosts = async () => { 19 | try { 20 | const response = await postsAPI.getAll(); 21 | setPosts(response.data.filter(p => p.scheduled_time)); 22 | } catch (error) { 23 | console.error('Error loading posts:', error); 24 | } finally { 25 | setLoading(false); 26 | } 27 | }; 28 | 29 | const getPostsForDate = (date) => { 30 | return posts.filter(post => { 31 | if (!post.scheduled_time) return false; 32 | const postDate = new Date(post.scheduled_time); 33 | return postDate.toDateString() === date.toDateString(); 34 | }); 35 | }; 36 | 37 | const tileContent = ({ date }) => { 38 | const postsOnDate = getPostsForDate(date); 39 | if (postsOnDate.length > 0) { 40 | return ( 41 |
42 |
43 |
44 | ); 45 | } 46 | return null; 47 | }; 48 | 49 | const selectedDatePosts = getPostsForDate(selectedDate); 50 | 51 | if (loading) { 52 | return ( 53 |
54 |
55 |
56 | ); 57 | } 58 | 59 | return ( 60 |
61 | 62 |
63 | 64 |
65 |
66 |
67 | 68 |

Scheduled Posts Calendar

69 |
70 | 71 |
72 |
73 | 79 |
80 | 81 |
82 |

83 | Posts on {selectedDate.toLocaleDateString()} 84 |

85 | 86 |
87 | {selectedDatePosts.length === 0 ? ( 88 |

89 | No posts scheduled for this date 90 |

91 | ) : ( 92 | selectedDatePosts.map((post) => ( 93 |
94 |
95 | 99 | {post.status} 100 | 101 | 102 | {new Date(post.scheduled_time).toLocaleTimeString()} 103 | 104 |
105 |

{post.content}

106 | {post.platforms.length > 0 && ( 107 |
108 | {post.platforms.map((platform, index) => ( 109 | 110 | {platform} 111 | 112 | ))} 113 |
114 | )} 115 |
116 | )) 117 | )} 118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /frontend/src/pages/Register.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useAuth } from '../context/AuthContext'; 4 | import { UserPlus } from 'lucide-react'; 5 | 6 | export default function Register() { 7 | const [email, setEmail] = useState(''); 8 | const [username, setUsername] = useState(''); 9 | const [password, setPassword] = useState(''); 10 | const [confirmPassword, setConfirmPassword] = useState(''); 11 | const [error, setError] = useState(''); 12 | const [loading, setLoading] = useState(false); 13 | const { register } = useAuth(); 14 | const navigate = useNavigate(); 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault(); 18 | setError(''); 19 | 20 | if (password !== confirmPassword) { 21 | setError('Passwords do not match'); 22 | return; 23 | } 24 | 25 | if (password.length < 6) { 26 | setError('Password must be at least 6 characters'); 27 | return; 28 | } 29 | 30 | setLoading(true); 31 | 32 | try { 33 | await register(email, username, password); 34 | navigate('/login'); 35 | } catch (err) { 36 | setError(err.response?.data?.detail || 'Registration failed. Please try again.'); 37 | } finally { 38 | setLoading(false); 39 | } 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |

Create Account

52 |

Join us and automate your social media

53 |
54 | 55 | {error && ( 56 |
57 | {error} 58 |
59 | )} 60 | 61 |
62 |
63 | 66 | setEmail(e.target.value)} 71 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 72 | placeholder="your@email.com" 73 | /> 74 |
75 | 76 |
77 | 80 | setUsername(e.target.value)} 85 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 86 | placeholder="username" 87 | /> 88 |
89 | 90 |
91 | 94 | setPassword(e.target.value)} 99 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 100 | placeholder="••••••••" 101 | /> 102 |
103 | 104 |
105 | 108 | setConfirmPassword(e.target.value)} 113 | className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent" 114 | placeholder="••••••••" 115 | /> 116 |
117 | 118 | 125 |
126 | 127 |

128 | Already have an account?{' '} 129 | 130 | Sign in 131 | 132 |

133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /backend/app/routers/posts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from sqlalchemy.orm import Session 3 | from typing import List, Dict 4 | from datetime import datetime 5 | from ..database import get_db 6 | from ..models import User, Post, SocialAccount 7 | from ..schemas import PostCreate, PostUpdate, PostResponse 8 | from ..auth import get_current_active_user 9 | from ..social_media_integrations import SocialMediaPublisher 10 | 11 | router = APIRouter(prefix="/posts", tags=["posts"]) 12 | 13 | @router.get("/", response_model=List[PostResponse]) 14 | async def get_posts( 15 | current_user: User = Depends(get_current_active_user), 16 | db: Session = Depends(get_db) 17 | ): 18 | posts = db.query(Post).filter(Post.user_id == current_user.id).order_by(Post.created_at.desc()).all() 19 | return posts 20 | 21 | @router.get("/{post_id}", response_model=PostResponse) 22 | async def get_post( 23 | post_id: int, 24 | current_user: User = Depends(get_current_active_user), 25 | db: Session = Depends(get_db) 26 | ): 27 | post = db.query(Post).filter( 28 | Post.id == post_id, 29 | Post.user_id == current_user.id 30 | ).first() 31 | 32 | if not post: 33 | raise HTTPException(status_code=404, detail="Post not found") 34 | 35 | return post 36 | 37 | @router.post("/", response_model=PostResponse) 38 | async def create_post( 39 | post_data: PostCreate, 40 | current_user: User = Depends(get_current_active_user), 41 | db: Session = Depends(get_db) 42 | ): 43 | new_post = Post( 44 | user_id=current_user.id, 45 | content=post_data.content, 46 | hashtags=post_data.hashtags, 47 | platforms=post_data.platforms, 48 | scheduled_time=post_data.scheduled_time, 49 | status="scheduled" if post_data.scheduled_time else "draft" 50 | ) 51 | db.add(new_post) 52 | db.commit() 53 | db.refresh(new_post) 54 | return new_post 55 | 56 | @router.patch("/{post_id}", response_model=PostResponse) 57 | async def update_post( 58 | post_id: int, 59 | post_data: PostUpdate, 60 | current_user: User = Depends(get_current_active_user), 61 | db: Session = Depends(get_db) 62 | ): 63 | post = db.query(Post).filter( 64 | Post.id == post_id, 65 | Post.user_id == current_user.id 66 | ).first() 67 | 68 | if not post: 69 | raise HTTPException(status_code=404, detail="Post not found") 70 | 71 | if post_data.content is not None: 72 | post.content = post_data.content 73 | if post_data.hashtags is not None: 74 | post.hashtags = post_data.hashtags 75 | if post_data.platforms is not None: 76 | post.platforms = post_data.platforms 77 | if post_data.scheduled_time is not None: 78 | post.scheduled_time = post_data.scheduled_time 79 | post.status = "scheduled" 80 | if post_data.status is not None: 81 | post.status = post_data.status 82 | 83 | post.updated_at = datetime.utcnow() 84 | db.commit() 85 | db.refresh(post) 86 | return post 87 | 88 | @router.delete("/{post_id}") 89 | async def delete_post( 90 | post_id: int, 91 | current_user: User = Depends(get_current_active_user), 92 | db: Session = Depends(get_db) 93 | ): 94 | post = db.query(Post).filter( 95 | Post.id == post_id, 96 | Post.user_id == current_user.id 97 | ).first() 98 | 99 | if not post: 100 | raise HTTPException(status_code=404, detail="Post not found") 101 | 102 | db.delete(post) 103 | db.commit() 104 | return {"message": "Post deleted successfully"} 105 | 106 | @router.post("/{post_id}/publish") 107 | async def publish_post( 108 | post_id: int, 109 | current_user: User = Depends(get_current_active_user), 110 | db: Session = Depends(get_db) 111 | ): 112 | post = db.query(Post).filter( 113 | Post.id == post_id, 114 | Post.user_id == current_user.id 115 | ).first() 116 | 117 | if not post: 118 | raise HTTPException(status_code=404, detail="Post not found") 119 | 120 | # Fetch user's social account tokens 121 | social_accounts = db.query(SocialAccount).filter( 122 | SocialAccount.user_id == current_user.id, 123 | SocialAccount.is_connected == True 124 | ).all() 125 | 126 | # Build tokens dictionary from social accounts 127 | user_tokens: Dict[str, str] = {} 128 | for account in social_accounts: 129 | platform_lower = account.platform.lower() 130 | if account.access_token: 131 | if platform_lower in ['threads']: 132 | user_tokens['threads'] = account.access_token 133 | elif platform_lower in ['instagram']: 134 | user_tokens['instagram'] = account.access_token 135 | 136 | # Initialize the social media publisher 137 | publisher = SocialMediaPublisher() 138 | 139 | # Publish to the selected platforms 140 | publishing_results = publisher.publish_to_platforms( 141 | content=post.content, 142 | platforms=post.platforms or [], 143 | user_tokens=user_tokens if user_tokens else None, 144 | image_url=None # TODO: Add image support if needed 145 | ) 146 | 147 | # Check if at least one platform succeeded 148 | any_success = any(result.get("success", False) for result in publishing_results) 149 | 150 | if any_success: 151 | post.is_published = True 152 | post.status = "published" 153 | db.commit() 154 | db.refresh(post) 155 | else: 156 | post.status = "failed" 157 | db.commit() 158 | db.refresh(post) 159 | 160 | return { 161 | "message": "Publishing complete" if any_success else "Publishing failed", 162 | "post": post, 163 | "publishing_results": publishing_results 164 | } 165 | -------------------------------------------------------------------------------- /frontend/src/pages/Posts.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { postsAPI } from '../services/api'; 3 | import Navbar from '../components/Navbar'; 4 | import Sidebar from '../components/Sidebar'; 5 | import { FileText, Trash2, Send } from 'lucide-react'; 6 | 7 | export default function Posts() { 8 | const [posts, setPosts] = useState([]); 9 | const [loading, setLoading] = useState(true); 10 | const [selectedPost, setSelectedPost] = useState(null); 11 | 12 | useEffect(() => { 13 | loadPosts(); 14 | }, []); 15 | 16 | const loadPosts = async () => { 17 | try { 18 | const response = await postsAPI.getAll(); 19 | setPosts(response.data); 20 | } catch (error) { 21 | console.error('Error loading posts:', error); 22 | } finally { 23 | setLoading(false); 24 | } 25 | }; 26 | 27 | const handleDelete = async (id) => { 28 | if (window.confirm('Are you sure you want to delete this post?')) { 29 | try { 30 | await postsAPI.delete(id); 31 | loadPosts(); 32 | } catch (error) { 33 | console.error('Error deleting post:', error); 34 | } 35 | } 36 | }; 37 | 38 | const handlePublish = async (id) => { 39 | if (window.confirm('Publish this post now? (This is a simulated action in the MVP)')) { 40 | try { 41 | await postsAPI.publish(id); 42 | loadPosts(); 43 | } catch (error) { 44 | console.error('Error publishing post:', error); 45 | } 46 | } 47 | }; 48 | 49 | if (loading) { 50 | return ( 51 |
52 |
53 |
54 | ); 55 | } 56 | 57 | return ( 58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | 66 |

My Posts

67 |
68 | 69 |
70 | {posts.length === 0 ? ( 71 |
72 | 73 |

No posts yet

74 |

Generate your first post to get started!

75 |
76 | ) : ( 77 | posts.map((post) => ( 78 |
79 |
80 |
81 |
82 | 87 | {post.status} 88 | 89 | 90 | Created: {new Date(post.created_at).toLocaleDateString()} 91 | 92 |
93 |

{post.content}

94 | 95 | {post.hashtags.length > 0 && ( 96 |
97 | {post.hashtags.map((hashtag, index) => ( 98 | 99 | {hashtag} 100 | 101 | ))} 102 |
103 | )} 104 | 105 | {post.platforms.length > 0 && ( 106 |
107 | Platforms: 108 | {post.platforms.map((platform, index) => ( 109 | 110 | {platform} 111 | 112 | ))} 113 |
114 | )} 115 | 116 | {post.scheduled_time && ( 117 |

118 | Scheduled for: {new Date(post.scheduled_time).toLocaleString()} 119 |

120 | )} 121 |
122 | 123 |
124 | {!post.is_published && ( 125 | 132 | )} 133 | 140 |
141 |
142 |
143 | )) 144 | )} 145 |
146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /frontend/src/pages/GenerateContent.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { contentAPI, postsAPI } from '../services/api'; 4 | import Navbar from '../components/Navbar'; 5 | import Sidebar from '../components/Sidebar'; 6 | import { Sparkles, RefreshCw, Save } from 'lucide-react'; 7 | 8 | export default function GenerateContent() { 9 | const navigate = useNavigate(); 10 | const [topic, setTopic] = useState(''); 11 | const [platform, setPlatform] = useState('twitter'); 12 | const [generatedContent, setGeneratedContent] = useState(null); 13 | const [generating, setGenerating] = useState(false); 14 | const [saving, setSaving] = useState(false); 15 | const [error, setError] = useState(''); 16 | 17 | const handleGenerate = async () => { 18 | if (!topic.trim()) { 19 | setError('Please enter a topic'); 20 | return; 21 | } 22 | 23 | setGenerating(true); 24 | setError(''); 25 | try { 26 | const response = await contentAPI.generate({ topic, platform }); 27 | setGeneratedContent(response.data); 28 | } catch (err) { 29 | setError(err.response?.data?.detail || 'Failed to generate content. Please try again.'); 30 | } finally { 31 | setGenerating(false); 32 | } 33 | }; 34 | 35 | const handleSave = async () => { 36 | setSaving(true); 37 | try { 38 | await postsAPI.create({ 39 | content: generatedContent.content, 40 | hashtags: generatedContent.hashtags, 41 | platforms: [platform], 42 | }); 43 | navigate('/posts'); 44 | } catch (err) { 45 | setError('Failed to save post. Please try again.'); 46 | } finally { 47 | setSaving(false); 48 | } 49 | }; 50 | 51 | return ( 52 |
53 | 54 |
55 | 56 |
57 |
58 |
59 | 60 |

Generate Content

61 |
62 | 63 | {error && ( 64 |
65 | {error} 66 |
67 | )} 68 | 69 |
70 |
71 | 74 | setTopic(e.target.value)} 78 | placeholder="Enter a topic (e.g., AI in marketing, productivity tips)" 79 | className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-600" 80 | /> 81 |
82 | 83 |
84 | 87 | 97 |
98 | 99 | 107 |
108 | 109 | {generatedContent && ( 110 |
111 |
112 |

Generated Content

113 | 121 |
122 | 123 |
124 |

{generatedContent.content}

125 |
126 | 127 |
128 |

Suggested Hashtags:

129 |
130 | {generatedContent.hashtags.map((hashtag, index) => ( 131 | 132 | {hashtag} 133 | 134 | ))} 135 |
136 |
137 | 138 |
139 | 147 |
148 |
149 | )} 150 |
151 |
152 |
153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /backend/app/social_media_integrations.py: -------------------------------------------------------------------------------- 1 | import tweepy 2 | import requests 3 | from typing import Dict, List, Optional 4 | from .config import settings 5 | 6 | 7 | class XTwitterIntegration: 8 | """Integration for posting to X/Twitter""" 9 | 10 | def __init__(self): 11 | # Validate credentials before initializing client 12 | if not all([settings.x_api_key, settings.x_api_secret, 13 | settings.x_access_token, settings.x_access_token_secret]): 14 | self.client = None 15 | self.credentials_missing = True 16 | else: 17 | try: 18 | self.client = tweepy.Client( 19 | bearer_token=settings.x_bearer_token if settings.x_bearer_token else None, 20 | consumer_key=settings.x_api_key, 21 | consumer_secret=settings.x_api_secret, 22 | access_token=settings.x_access_token, 23 | access_token_secret=settings.x_access_token_secret 24 | ) 25 | self.credentials_missing = False 26 | except Exception as e: 27 | self.client = None 28 | self.credentials_missing = True 29 | self.init_error = str(e) 30 | 31 | def post_tweet(self, content: str) -> Dict: 32 | """Post a tweet to X/Twitter""" 33 | if self.credentials_missing or not self.client: 34 | return { 35 | "success": False, 36 | "platform": "X/Twitter", 37 | "error": "Missing or invalid X/Twitter API credentials", 38 | "message": "X/Twitter credentials are not properly configured. Please add them to Replit Secrets." 39 | } 40 | 41 | try: 42 | response = self.client.create_tweet(text=content) 43 | return { 44 | "success": True, 45 | "platform": "X/Twitter", 46 | "post_id": response.data['id'] if response.data else None, 47 | "message": "Successfully posted to X/Twitter" 48 | } 49 | except Exception as e: 50 | return { 51 | "success": False, 52 | "platform": "X/Twitter", 53 | "error": str(e), 54 | "message": f"Failed to post to X/Twitter: {str(e)}" 55 | } 56 | 57 | 58 | class ThreadsIntegration: 59 | """Integration for posting to Threads (Meta)""" 60 | 61 | def __init__(self): 62 | self.app_id = settings.threads_app_id 63 | self.app_secret = settings.threads_app_secret 64 | self.base_url = "https://graph.threads.net/v1.0" 65 | 66 | def post_thread(self, content: str, user_access_token: Optional[str] = None) -> Dict: 67 | """Post to Threads""" 68 | try: 69 | if not user_access_token: 70 | return { 71 | "success": False, 72 | "platform": "Threads", 73 | "error": "User access token required", 74 | "message": "Threads requires user-specific access token. Please connect your Threads account." 75 | } 76 | 77 | url = f"{self.base_url}/me/threads" 78 | payload = { 79 | "text": content, 80 | "access_token": user_access_token 81 | } 82 | 83 | response = requests.post(url, json=payload) 84 | 85 | if response.status_code == 200: 86 | return { 87 | "success": True, 88 | "platform": "Threads", 89 | "post_id": response.json().get("id"), 90 | "message": "Successfully posted to Threads" 91 | } 92 | else: 93 | return { 94 | "success": False, 95 | "platform": "Threads", 96 | "error": response.text, 97 | "message": f"Failed to post to Threads: {response.text}" 98 | } 99 | except Exception as e: 100 | return { 101 | "success": False, 102 | "platform": "Threads", 103 | "error": str(e), 104 | "message": f"Failed to post to Threads: {str(e)}" 105 | } 106 | 107 | 108 | class InstagramIntegration: 109 | """Integration for posting to Instagram (Meta)""" 110 | 111 | def __init__(self): 112 | self.app_id = settings.instagram_app_id 113 | self.app_secret = settings.instagram_app_secret 114 | self.base_url = "https://graph.instagram.com/v18.0" 115 | 116 | def post_to_instagram(self, content: str, image_url: Optional[str] = None, user_access_token: Optional[str] = None) -> Dict: 117 | """Post to Instagram using proper Media API flow""" 118 | try: 119 | if not user_access_token: 120 | return { 121 | "success": False, 122 | "platform": "Instagram", 123 | "error": "User access token required", 124 | "message": "Instagram requires user-specific access token. Please connect your Instagram account." 125 | } 126 | 127 | if not image_url: 128 | return { 129 | "success": False, 130 | "platform": "Instagram", 131 | "error": "Image required", 132 | "message": "Instagram posts require an image URL" 133 | } 134 | 135 | # Step 1: Create media container 136 | container_url = f"{self.base_url}/me/media" 137 | container_payload = { 138 | "image_url": image_url, 139 | "caption": content, 140 | "access_token": user_access_token 141 | } 142 | 143 | container_response = requests.post(container_url, data=container_payload) 144 | 145 | if container_response.status_code != 200: 146 | return { 147 | "success": False, 148 | "platform": "Instagram", 149 | "error": container_response.text, 150 | "message": f"Failed to create Instagram media container: {container_response.text}" 151 | } 152 | 153 | container_id = container_response.json().get("id") 154 | 155 | # Step 2: Publish the media container 156 | publish_url = f"{self.base_url}/me/media_publish" 157 | publish_payload = { 158 | "creation_id": container_id, 159 | "access_token": user_access_token 160 | } 161 | 162 | publish_response = requests.post(publish_url, data=publish_payload) 163 | 164 | if publish_response.status_code == 200: 165 | return { 166 | "success": True, 167 | "platform": "Instagram", 168 | "post_id": publish_response.json().get("id"), 169 | "message": "Successfully posted to Instagram" 170 | } 171 | else: 172 | return { 173 | "success": False, 174 | "platform": "Instagram", 175 | "error": publish_response.text, 176 | "message": f"Failed to publish Instagram media: {publish_response.text}" 177 | } 178 | except Exception as e: 179 | return { 180 | "success": False, 181 | "platform": "Instagram", 182 | "error": str(e), 183 | "message": f"Failed to post to Instagram: {str(e)}" 184 | } 185 | 186 | 187 | class SocialMediaPublisher: 188 | """Main class to publish to multiple social media platforms""" 189 | 190 | def __init__(self): 191 | self.x_twitter = XTwitterIntegration() 192 | self.threads = ThreadsIntegration() 193 | self.instagram = InstagramIntegration() 194 | 195 | def publish_to_platforms(self, content: str, platforms: List[str], 196 | user_tokens: Optional[Dict[str, str]] = None, 197 | image_url: Optional[str] = None) -> List[Dict]: 198 | """Publish content to multiple platforms""" 199 | results = [] 200 | 201 | for platform in platforms: 202 | platform_lower = platform.lower() 203 | 204 | if platform_lower in ['x', 'twitter', 'x/twitter']: 205 | result = self.x_twitter.post_tweet(content) 206 | results.append(result) 207 | 208 | elif platform_lower in ['threads']: 209 | token = user_tokens.get('threads') if user_tokens else None 210 | result = self.threads.post_thread(content, token) 211 | results.append(result) 212 | 213 | elif platform_lower in ['instagram']: 214 | token = user_tokens.get('instagram') if user_tokens else None 215 | result = self.instagram.post_to_instagram(content, image_url, token) 216 | results.append(result) 217 | 218 | else: 219 | results.append({ 220 | "success": False, 221 | "platform": platform, 222 | "error": f"Unsupported platform: {platform}", 223 | "message": f"Platform {platform} is not supported yet" 224 | }) 225 | 226 | return results 227 | -------------------------------------------------------------------------------- /frontend/src/pages/Preferences.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { preferencesAPI } from '../services/api'; 4 | import Navbar from '../components/Navbar'; 5 | import Sidebar from '../components/Sidebar'; 6 | import { Settings, Save } from 'lucide-react'; 7 | 8 | export default function Preferences() { 9 | const navigate = useNavigate(); 10 | const [preferences, setPreferences] = useState({ 11 | topics: [], 12 | hashtags: [], 13 | posting_style: 'professional', 14 | tone: 'friendly', 15 | content_length: 'medium', 16 | include_emojis: true, 17 | }); 18 | const [topicInput, setTopicInput] = useState(''); 19 | const [hashtagInput, setHashtagInput] = useState(''); 20 | const [loading, setLoading] = useState(true); 21 | const [saving, setSaving] = useState(false); 22 | const [message, setMessage] = useState(''); 23 | 24 | useEffect(() => { 25 | loadPreferences(); 26 | }, []); 27 | 28 | const loadPreferences = async () => { 29 | try { 30 | const response = await preferencesAPI.get(); 31 | setPreferences(response.data); 32 | } catch (error) { 33 | console.error('Error loading preferences:', error); 34 | } finally { 35 | setLoading(false); 36 | } 37 | }; 38 | 39 | const handleAddTopic = (e) => { 40 | e.preventDefault(); 41 | if (topicInput.trim() && !preferences.topics.includes(topicInput.trim())) { 42 | setPreferences({ 43 | ...preferences, 44 | topics: [...preferences.topics, topicInput.trim()], 45 | }); 46 | setTopicInput(''); 47 | } 48 | }; 49 | 50 | const handleRemoveTopic = (topic) => { 51 | setPreferences({ 52 | ...preferences, 53 | topics: preferences.topics.filter((t) => t !== topic), 54 | }); 55 | }; 56 | 57 | const handleAddHashtag = (e) => { 58 | e.preventDefault(); 59 | let hashtag = hashtagInput.trim(); 60 | if (!hashtag.startsWith('#')) hashtag = '#' + hashtag; 61 | if (hashtag.length > 1 && !preferences.hashtags.includes(hashtag)) { 62 | setPreferences({ 63 | ...preferences, 64 | hashtags: [...preferences.hashtags, hashtag], 65 | }); 66 | setHashtagInput(''); 67 | } 68 | }; 69 | 70 | const handleRemoveHashtag = (hashtag) => { 71 | setPreferences({ 72 | ...preferences, 73 | hashtags: preferences.hashtags.filter((h) => h !== hashtag), 74 | }); 75 | }; 76 | 77 | const handleSave = async () => { 78 | setSaving(true); 79 | setMessage(''); 80 | try { 81 | await preferencesAPI.update(preferences); 82 | setMessage('Preferences saved successfully!'); 83 | setTimeout(() => navigate('/dashboard'), 1500); 84 | } catch (error) { 85 | setMessage('Error saving preferences. Please try again.'); 86 | } finally { 87 | setSaving(false); 88 | } 89 | }; 90 | 91 | if (loading) { 92 | return ( 93 |
94 |
95 |
96 | ); 97 | } 98 | 99 | return ( 100 |
101 | 102 |
103 | 104 |
105 |
106 |
107 | 108 |

Content Preferences

109 |
110 | 111 | {message && ( 112 |
115 | {message} 116 |
117 | )} 118 | 119 |
120 |
121 | 124 |
125 | setTopicInput(e.target.value)} 129 | placeholder="Add a topic (e.g., Technology, Marketing)" 130 | className="flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-600" 131 | /> 132 | 135 |
136 |
137 | {preferences.topics.map((topic) => ( 138 | 142 | {topic} 143 | 146 | 147 | ))} 148 |
149 |
150 | 151 |
152 | 155 |
156 | setHashtagInput(e.target.value)} 160 | placeholder="Add a hashtag (e.g., marketing)" 161 | className="flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-600" 162 | /> 163 | 166 |
167 |
168 | {preferences.hashtags.map((hashtag) => ( 169 | 173 | {hashtag} 174 | 177 | 178 | ))} 179 |
180 |
181 | 182 |
183 | 186 | 197 |
198 | 199 |
200 | 203 | 214 |
215 | 216 |
217 | 220 | 229 |
230 | 231 |
232 | setPreferences({ ...preferences, include_emojis: e.target.checked })} 237 | className="h-4 w-4 text-indigo-600 rounded" 238 | /> 239 | 242 |
243 | 244 | 252 |
253 |
254 |
255 |
256 |
257 | ); 258 | } 259 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useAuth } from '../context/AuthContext'; 4 | import { socialAccountsAPI, postsAPI } from '../services/api'; 5 | import Navbar from '../components/Navbar'; 6 | import Sidebar from '../components/Sidebar'; 7 | import { Plus, Twitter, Facebook, Instagram, Linkedin, Calendar, FileText } from 'lucide-react'; 8 | 9 | const platformIcons = { 10 | twitter: Twitter, 11 | facebook: Facebook, 12 | instagram: Instagram, 13 | linkedin: Linkedin, 14 | }; 15 | 16 | export default function Dashboard() { 17 | const { user } = useAuth(); 18 | const [socialAccounts, setSocialAccounts] = useState([]); 19 | const [posts, setPosts] = useState([]); 20 | const [showAddAccount, setShowAddAccount] = useState(false); 21 | const [newAccount, setNewAccount] = useState({ platform: 'twitter', account_name: '' }); 22 | const [loading, setLoading] = useState(true); 23 | 24 | useEffect(() => { 25 | loadData(); 26 | }, []); 27 | 28 | const loadData = async () => { 29 | try { 30 | const [accountsRes, postsRes] = await Promise.all([ 31 | socialAccountsAPI.getAll(), 32 | postsAPI.getAll(), 33 | ]); 34 | setSocialAccounts(accountsRes.data); 35 | setPosts(postsRes.data); 36 | } catch (error) { 37 | console.error('Error loading data:', error); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | const handleAddAccount = async (e) => { 44 | e.preventDefault(); 45 | try { 46 | await socialAccountsAPI.create(newAccount); 47 | setNewAccount({ platform: 'twitter', account_name: '' }); 48 | setShowAddAccount(false); 49 | loadData(); 50 | } catch (error) { 51 | console.error('Error adding account:', error); 52 | } 53 | }; 54 | 55 | const handleDeleteAccount = async (id) => { 56 | if (window.confirm('Are you sure you want to remove this account?')) { 57 | try { 58 | await socialAccountsAPI.delete(id); 59 | loadData(); 60 | } catch (error) { 61 | console.error('Error deleting account:', error); 62 | } 63 | } 64 | }; 65 | 66 | if (loading) { 67 | return ( 68 |
69 |
70 |
71 | ); 72 | } 73 | 74 | return ( 75 |
76 | 77 |
78 | 79 |
80 |
81 |

Dashboard

82 | 83 |
84 |
85 |
86 |
87 |

Connected Accounts

88 |

{socialAccounts.length}

89 |
90 |
91 | 92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |

Total Posts

100 |

{posts.length}

101 |
102 |
103 | 104 |
105 |
106 |
107 | 108 |
109 |
110 |
111 |

Scheduled

112 |

113 | {posts.filter(p => p.status === 'scheduled').length} 114 |

115 |
116 |
117 | 118 |
119 |
120 |
121 |
122 | 123 |
124 |
125 |
126 |

Social Accounts

127 | 134 |
135 | 136 | {showAddAccount && ( 137 |
138 |
139 | 149 | setNewAccount({ ...newAccount, account_name: e.target.value })} 154 | className="w-full px-3 py-2 border rounded-lg" 155 | required 156 | /> 157 |
158 | 161 | 168 |
169 |
170 |
171 | )} 172 | 173 |
174 | {socialAccounts.length === 0 ? ( 175 |

No social accounts connected yet

176 | ) : ( 177 | socialAccounts.map((account) => { 178 | const Icon = platformIcons[account.platform] || Twitter; 179 | return ( 180 |
181 |
182 | 183 |
184 |

{account.platform}

185 |

@{account.account_name}

186 |
187 |
188 | 194 |
195 | ); 196 | }) 197 | )} 198 |
199 |
200 | 201 |
202 |
203 |

Recent Posts

204 | 205 | View All 206 | 207 |
208 | 209 |
210 | {posts.length === 0 ? ( 211 |

No posts yet

212 | ) : ( 213 | posts.slice(0, 5).map((post) => ( 214 |
215 |

{post.content}

216 |
217 | 222 | {post.status} 223 | 224 | 225 | {new Date(post.created_at).toLocaleDateString()} 226 | 227 |
228 |
229 | )) 230 | )} 231 |
232 |
233 |
234 | 235 |
236 |

Quick Actions

237 |
238 | 242 |

Generate Content

243 |

Create AI-powered posts

244 | 245 | 249 |

Set Preferences

250 |

Configure content style

251 | 252 | 256 |

View Calendar

257 |

Schedule your posts

258 | 259 |
260 |
261 |
262 |
263 |
264 |
265 | ); 266 | } 267 | --------------------------------------------------------------------------------