Seth commited on
Commit
f80e9b3
·
1 Parent(s): cf6980b
Files changed (44) hide show
  1. README.md +264 -5
  2. backend/app/__init__.py +2 -0
  3. backend/app/main.py +210 -6
  4. backend/app/models.py +86 -0
  5. backend/app/schemas.py +115 -0
  6. backend/app/services/__init__.py +2 -0
  7. backend/app/services/ai_service.py +91 -0
  8. backend/app/services/canva_service.py +82 -0
  9. backend/app/services/linkedin_service.py +113 -0
  10. backend/requirements.txt +12 -1
  11. frontend/index.html +1 -1
  12. frontend/package.json +28 -3
  13. frontend/postcss.config.js +7 -0
  14. frontend/src/App.jsx +21 -19
  15. frontend/src/components/Layout.jsx +219 -0
  16. frontend/src/components/ui/alert.jsx +50 -0
  17. frontend/src/components/ui/avatar.jsx +39 -0
  18. frontend/src/components/ui/badge.jsx +32 -0
  19. frontend/src/components/ui/button.jsx +50 -0
  20. frontend/src/components/ui/calendar.jsx +54 -0
  21. frontend/src/components/ui/card.jsx +61 -0
  22. frontend/src/components/ui/checkbox.jsx +25 -0
  23. frontend/src/components/ui/dialog.jsx +99 -0
  24. frontend/src/components/ui/dropdown-menu.jsx +160 -0
  25. frontend/src/components/ui/input.jsx +20 -0
  26. frontend/src/components/ui/label.jsx +18 -0
  27. frontend/src/components/ui/popover.jsx +24 -0
  28. frontend/src/components/ui/progress.jsx +23 -0
  29. frontend/src/components/ui/select.jsx +135 -0
  30. frontend/src/components/ui/separator.jsx +26 -0
  31. frontend/src/components/ui/slider.jsx +23 -0
  32. frontend/src/components/ui/switch.jsx +24 -0
  33. frontend/src/components/ui/tabs.jsx +43 -0
  34. frontend/src/components/ui/textarea.jsx +19 -0
  35. frontend/src/index.css +38 -0
  36. frontend/src/main.jsx +1 -0
  37. frontend/src/pages/Dashboard.jsx +252 -0
  38. frontend/src/pages/Integrations.jsx +420 -0
  39. frontend/src/pages/PostEditor.jsx +513 -0
  40. frontend/src/pages/Repository.jsx +510 -0
  41. frontend/src/pages/Scheduler.jsx +493 -0
  42. frontend/src/utils.js +18 -0
  43. frontend/tailwind.config.js +12 -0
  44. frontend/vite.config.js +6 -0
README.md CHANGED
@@ -1,10 +1,269 @@
1
  ---
2
- title: PostGen
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PostGen - LinkedIn Content Scheduler
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # PostGen - AI-Powered LinkedIn Content Scheduler
11
+
12
+ PostGen is a comprehensive LinkedIn content scheduling application that integrates with Canva and LinkedIn APIs to automate content creation and posting. The app uses GPT for AI-generated content and Canva brand templates for consistent visual design.
13
+
14
+ ## Features
15
+
16
+ - **AI Content Generation**: Uses GPT to generate engaging LinkedIn posts
17
+ - **Canva Integration**: Access and apply Canva brand templates using the Autofill API
18
+ - **LinkedIn Scheduling**: Schedule and publish posts directly to LinkedIn
19
+ - **Asset Repository**: Upload and organize marketing materials by product categories
20
+ - **Smart Scheduler**: Agentic AI automatically generates content schedules based on date ranges, products, and post types
21
+ - **Product Categories**: Support for OCR, P2P, and O2C products with sub-categories
22
+
23
+ ## Tech Stack
24
+
25
+ - **Frontend**: React, React Router, Tailwind CSS, Framer Motion, shadcn/ui
26
+ - **Backend**: FastAPI, Python
27
+ - **AI**: OpenAI GPT (latest model)
28
+ - **APIs**: Canva Connect API, LinkedIn API
29
+
30
+ ## Setup Instructions
31
+
32
+ ### Prerequisites
33
+
34
+ 1. **Canva Account**:
35
+ - Canva Teams account (free trial available)
36
+ - Canva Connect API integration created
37
+ - Autofill API access registered
38
+
39
+ 2. **LinkedIn Account**:
40
+ - LinkedIn Developer account
41
+ - LinkedIn App created with appropriate permissions
42
+
43
+ 3. **OpenAI Account**:
44
+ - OpenAI API key with access to GPT models
45
+
46
+ ### Environment Variables
47
+
48
+ Create a `.env` file in the backend directory with the following variables:
49
+
50
+ ```env
51
+ # OpenAI
52
+ OPENAI_API_KEY=your_openai_api_key
53
+ OPENAI_MODEL=gpt-4o
54
+
55
+ # Canva (optional - can be passed via API)
56
+ CANVA_ACCESS_TOKEN=your_canva_access_token
57
+
58
+ # LinkedIn (optional - can be passed via API)
59
+ LINKEDIN_ACCESS_TOKEN=your_linkedin_access_token
60
+ LINKEDIN_PERSON_URN=your_linkedin_person_urn
61
+
62
+ # Database (for production)
63
+ DATABASE_URL=postgresql://user:password@localhost/postgen
64
+ ```
65
+
66
+ ### Local Development
67
+
68
+ 1. **Install Frontend Dependencies**:
69
+ ```bash
70
+ cd frontend
71
+ npm install
72
+ ```
73
+
74
+ 2. **Install Backend Dependencies**:
75
+ ```bash
76
+ cd backend
77
+ pip install -r requirements.txt
78
+ ```
79
+
80
+ 3. **Run Frontend** (development mode):
81
+ ```bash
82
+ cd frontend
83
+ npm run dev
84
+ ```
85
+
86
+ 4. **Run Backend**:
87
+ ```bash
88
+ cd backend
89
+ uvicorn app.main:app --reload --port 8000
90
+ ```
91
+
92
+ ### Building for Production
93
+
94
+ 1. **Build Frontend**:
95
+ ```bash
96
+ cd frontend
97
+ npm run build
98
+ ```
99
+
100
+ 2. **Build Docker Image**:
101
+ ```bash
102
+ docker build -t postgen .
103
+ ```
104
+
105
+ 3. **Run Docker Container**:
106
+ ```bash
107
+ docker run -p 7860:7860 --env-file .env postgen
108
+ ```
109
+
110
+ ## Deployment to HuggingFace Spaces
111
+
112
+ ### Step 1: Prepare Your Repository
113
+
114
+ 1. Ensure all code is committed and pushed to your Git repository
115
+ 2. Make sure the `Dockerfile` is in the root directory
116
+ 3. Ensure `README.md` has the HuggingFace Spaces configuration at the top
117
+
118
+ ### Step 2: Create a HuggingFace Space
119
+
120
+ 1. Go to [HuggingFace Spaces](https://huggingface.co/spaces)
121
+ 2. Click "Create new Space"
122
+ 3. Choose:
123
+ - **SDK**: Docker
124
+ - **Name**: postgen (or your preferred name)
125
+ - **Visibility**: Public or Private
126
+
127
+ ### Step 3: Configure Environment Variables
128
+
129
+ 1. In your HuggingFace Space settings, go to "Variables and secrets"
130
+ 2. Add the following secrets:
131
+ - `OPENAI_API_KEY`: Your OpenAI API key
132
+ - `OPENAI_MODEL`: gpt-4o (or your preferred model)
133
+ - `CANVA_ACCESS_TOKEN`: (Optional, can be set per user)
134
+ - `LINKEDIN_ACCESS_TOKEN`: (Optional, can be set per user)
135
+
136
+ ### Step 4: Connect Your Repository
137
+
138
+ 1. In Space settings, connect your Git repository
139
+ 2. Or push your code directly to the HuggingFace Space repository
140
+
141
+ ### Step 5: Deploy
142
+
143
+ 1. HuggingFace will automatically build and deploy your Docker image
144
+ 2. Monitor the build logs in the Space interface
145
+ 3. Once deployed, your app will be available at: `https://huggingface.co/spaces/your-username/postgen`
146
+
147
+ ## API Integration Guide
148
+
149
+ ### Canva Integration
150
+
151
+ 1. **Get Access Token**:
152
+ - Create a Canva Connect API integration
153
+ - Complete OAuth flow to get access token
154
+ - Token should have scopes: `design:content:write`, `design:content:read`, `brandtemplate:content:read`, `brandtemplate:meta:read`
155
+
156
+ 2. **Using Brand Templates**:
157
+ - Call `/api/canva/brand-templates` to get available templates
158
+ - Call `/api/canva/brand-templates/{id}/dataset` to get template structure
159
+ - Call `/api/canva/autofill` to create a design from template
160
+ - Poll `/api/canva/autofill/{job_id}` to check status
161
+
162
+ ### LinkedIn Integration
163
+
164
+ 1. **Get Access Token**:
165
+ - Create a LinkedIn App
166
+ - Request permissions: `w_member_social`, `r_liteprofile`
167
+ - Complete OAuth flow to get access token
168
+
169
+ 2. **Posting to LinkedIn**:
170
+ - Call `/api/linkedin/post` with your access token and post content
171
+ - Media can be included via `media_uris` parameter
172
+
173
+ ### AI Content Generation
174
+
175
+ 1. **Generate Content**:
176
+ - Call `/api/ai/generate-content` with:
177
+ - `product_category`: 'ocr', 'p2p', or 'o2c'
178
+ - `post_type`: 'carousel', 'cover_content', 'content_only', or 'webinar'
179
+ - `context`: Optional additional context
180
+ - `assets`: Optional list of asset IDs
181
+
182
+ ## Project Structure
183
+
184
+ ```
185
+ PostGen/
186
+ ├── frontend/
187
+ │ ├── src/
188
+ │ │ ├── components/
189
+ │ │ │ ├── ui/ # shadcn/ui components
190
+ │ │ │ └── Layout.jsx # Main layout component
191
+ │ │ ├── pages/
192
+ │ │ │ ├── Dashboard.jsx
193
+ │ │ │ ├── Repository.jsx
194
+ │ │ │ ├── Scheduler.jsx
195
+ │ │ │ ├── PostEditor.jsx
196
+ │ │ │ └── Integrations.jsx
197
+ │ │ ├── App.jsx
198
+ │ │ ├── main.jsx
199
+ │ │ └── utils.js
200
+ │ ├── package.json
201
+ │ └── vite.config.js
202
+ ├── backend/
203
+ │ ├── app/
204
+ │ │ ├── services/
205
+ │ │ │ ├── canva_service.py
206
+ │ │ │ ├── linkedin_service.py
207
+ │ │ │ └── ai_service.py
208
+ │ │ ├── models.py
209
+ │ │ ├── schemas.py
210
+ │ │ └── main.py
211
+ │ └── requirements.txt
212
+ ├── Dockerfile
213
+ └── README.md
214
+ ```
215
+
216
+ ## Next Steps After Deployment
217
+
218
+ 1. **Connect Integrations**:
219
+ - Go to the Integrations page
220
+ - Connect your Canva account
221
+ - Connect your LinkedIn account
222
+
223
+ 2. **Upload Assets**:
224
+ - Go to Repository page
225
+ - Upload marketing materials, screenshots, and documents
226
+ - Classify them by product category
227
+
228
+ 3. **Create Campaign**:
229
+ - Go to Scheduler page
230
+ - Click "Campaign Settings"
231
+ - Configure date range, products, post types, and frequency
232
+ - Click "Generate Schedule"
233
+
234
+ 4. **Review and Schedule**:
235
+ - Review AI-generated posts
236
+ - Edit content if needed
237
+ - Confirm and schedule posts
238
+
239
+ ## Troubleshooting
240
+
241
+ ### Build Issues
242
+
243
+ - **Frontend build fails**: Check Node.js version (requires 18+)
244
+ - **Backend import errors**: Ensure all Python packages are installed
245
+ - **Docker build fails**: Check Dockerfile syntax and paths
246
+
247
+ ### Runtime Issues
248
+
249
+ - **API errors**: Verify environment variables are set correctly
250
+ - **Canva API errors**: Check access token and scopes
251
+ - **LinkedIn API errors**: Verify OAuth permissions
252
+ - **AI generation fails**: Check OpenAI API key and quota
253
+
254
+ ### Common Issues
255
+
256
+ 1. **CORS errors**: Already handled in backend CORS middleware
257
+ 2. **Port conflicts**: HuggingFace Spaces uses port 7860
258
+ 3. **File uploads**: Ensure uploads directory has write permissions
259
+
260
+ ## Support
261
+
262
+ For issues or questions:
263
+ - Check the API documentation in the code
264
+ - Review Canva API docs: https://www.canva.dev/docs/connect/
265
+ - Review LinkedIn API docs: https://docs.microsoft.com/en-us/linkedin/
266
+
267
+ ## License
268
+
269
+ This project is private and proprietary.
backend/app/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # PostGen Backend Application
2
+
backend/app/main.py CHANGED
@@ -1,10 +1,22 @@
1
- from fastapi import FastAPI
2
- from fastapi.responses import FileResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
 
 
 
6
 
7
- app = FastAPI()
 
 
 
 
 
 
 
 
 
8
 
9
  app.add_middleware(
10
  CORSMiddleware,
@@ -14,14 +26,206 @@ app.add_middleware(
14
  allow_headers=["*"],
15
  )
16
 
17
- # ---- API ----
 
 
 
 
18
  @app.get("/api/health")
19
  def health():
20
- return {"status": "ok"}
21
 
22
  @app.get("/api/hello")
23
  def hello():
24
- return {"message": "Hello from FastAPI"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  # ---- Frontend static serving ----
27
  FRONTEND_DIST = Path(__file__).resolve().parents[2] / "frontend" / "dist"
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends
2
+ from fastapi.responses import FileResponse, JSONResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
6
+ from typing import List, Optional
7
+ import os
8
+ from datetime import datetime
9
 
10
+ from app.schemas import (
11
+ IntegrationResponse, AssetResponse, PostResponse, CampaignResponse,
12
+ CanvaBrandTemplate, CanvaAutofillRequest, CanvaAutofillResponse,
13
+ LinkedInPostRequest, AIContentRequest, AIContentResponse
14
+ )
15
+ from app.services.canva_service import CanvaService
16
+ from app.services.linkedin_service import LinkedInService
17
+ from app.services.ai_service import AIService
18
+
19
+ app = FastAPI(title="PostGen API", version="1.0.0")
20
 
21
  app.add_middleware(
22
  CORSMiddleware,
 
26
  allow_headers=["*"],
27
  )
28
 
29
+ # Services
30
+ ai_service = AIService()
31
+
32
+ # ---- API Endpoints ----
33
+
34
  @app.get("/api/health")
35
  def health():
36
+ return {"status": "ok", "message": "PostGen API is running"}
37
 
38
  @app.get("/api/hello")
39
  def hello():
40
+ return {"message": "Hello from PostGen API"}
41
+
42
+ # ---- Canva Integration ----
43
+
44
+ @app.get("/api/canva/brand-templates", response_model=List[CanvaBrandTemplate])
45
+ async def get_canva_brand_templates(access_token: str):
46
+ """Get list of Canva brand templates"""
47
+ try:
48
+ canva_service = CanvaService(access_token)
49
+ templates = await canva_service.get_brand_templates()
50
+ return templates
51
+ except Exception as e:
52
+ raise HTTPException(status_code=500, detail=str(e))
53
+
54
+ @app.get("/api/canva/brand-templates/{template_id}/dataset")
55
+ async def get_canva_template_dataset(template_id: str, access_token: str):
56
+ """Get dataset for a specific brand template"""
57
+ try:
58
+ canva_service = CanvaService(access_token)
59
+ dataset = await canva_service.get_brand_template_dataset(template_id)
60
+ return dataset
61
+ except Exception as e:
62
+ raise HTTPException(status_code=500, detail=str(e))
63
+
64
+ @app.post("/api/canva/autofill", response_model=CanvaAutofillResponse)
65
+ async def create_canva_autofill(request: CanvaAutofillRequest, access_token: str):
66
+ """Create an autofill job for a brand template"""
67
+ try:
68
+ canva_service = CanvaService(access_token)
69
+ response = await canva_service.create_autofill_job(request)
70
+ return response
71
+ except Exception as e:
72
+ raise HTTPException(status_code=500, detail=str(e))
73
+
74
+ @app.get("/api/canva/autofill/{job_id}")
75
+ async def get_canva_autofill_status(job_id: str, access_token: str):
76
+ """Get status of an autofill job"""
77
+ try:
78
+ canva_service = CanvaService(access_token)
79
+ status = await canva_service.get_autofill_job_status(job_id)
80
+ return status
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
84
+ # ---- LinkedIn Integration ----
85
+
86
+ @app.post("/api/linkedin/post")
87
+ async def create_linkedin_post(request: LinkedInPostRequest, access_token: str):
88
+ """Create a LinkedIn post"""
89
+ try:
90
+ linkedin_service = LinkedInService(access_token)
91
+ result = await linkedin_service.create_post(
92
+ text=request.text,
93
+ media_uris=request.media_uris
94
+ )
95
+ return result
96
+ except Exception as e:
97
+ raise HTTPException(status_code=500, detail=str(e))
98
+
99
+ @app.get("/api/linkedin/profile")
100
+ async def get_linkedin_profile(access_token: str):
101
+ """Get LinkedIn user profile"""
102
+ try:
103
+ linkedin_service = LinkedInService(access_token)
104
+ profile = await linkedin_service.get_user_profile()
105
+ return profile
106
+ except Exception as e:
107
+ raise HTTPException(status_code=500, detail=str(e))
108
+
109
+ # ---- AI Content Generation ----
110
+
111
+ @app.post("/api/ai/generate-content", response_model=AIContentResponse)
112
+ async def generate_ai_content(request: AIContentRequest):
113
+ """Generate LinkedIn post content using GPT"""
114
+ try:
115
+ # Get assets context if provided
116
+ assets_context = None
117
+ if request.assets:
118
+ # In a real implementation, fetch asset descriptions from database
119
+ assets_context = f"User has {len(request.assets)} assets available"
120
+
121
+ response = await ai_service.generate_content(request, assets_context)
122
+ return response
123
+ except Exception as e:
124
+ raise HTTPException(status_code=500, detail=f"AI generation failed: {str(e)}")
125
+
126
+ # ---- Asset Management ----
127
+
128
+ @app.post("/api/assets/upload")
129
+ async def upload_asset(
130
+ file: UploadFile = File(...),
131
+ product_category: str = None,
132
+ sub_category: Optional[str] = None
133
+ ):
134
+ """Upload an asset to the repository"""
135
+ try:
136
+ # Create uploads directory if it doesn't exist
137
+ upload_dir = Path("uploads")
138
+ upload_dir.mkdir(exist_ok=True)
139
+
140
+ # Save file
141
+ file_path = upload_dir / file.filename
142
+ with open(file_path, "wb") as buffer:
143
+ content = await file.read()
144
+ buffer.write(content)
145
+
146
+ # In a real implementation, save to database
147
+ return {
148
+ "id": 1,
149
+ "name": file.filename,
150
+ "file_type": file.content_type.split('/')[0] if file.content_type else "unknown",
151
+ "product_category": product_category,
152
+ "sub_category": sub_category,
153
+ "size": len(content),
154
+ "created_at": datetime.utcnow().isoformat()
155
+ }
156
+ except Exception as e:
157
+ raise HTTPException(status_code=500, detail=str(e))
158
+
159
+ @app.get("/api/assets", response_model=List[AssetResponse])
160
+ async def get_assets(product_category: Optional[str] = None):
161
+ """Get list of assets"""
162
+ # Mock data for now
163
+ return [
164
+ {
165
+ "id": 1,
166
+ "name": "OCR_Demo_Screenshot.png",
167
+ "file_type": "image",
168
+ "product_category": "ocr",
169
+ "sub_category": None,
170
+ "size": 2516582,
171
+ "created_at": datetime.utcnow()
172
+ }
173
+ ]
174
+
175
+ # ---- Post Management ----
176
+
177
+ @app.post("/api/posts", response_model=PostResponse)
178
+ async def create_post(post_data: dict):
179
+ """Create a new post"""
180
+ # In a real implementation, save to database
181
+ return {
182
+ "id": 1,
183
+ "title": post_data.get("title", "New Post"),
184
+ "content": post_data.get("content", ""),
185
+ "post_type": post_data.get("post_type", "content_only"),
186
+ "product_category": post_data.get("product_category", "ocr"),
187
+ "scheduled_date": post_data.get("scheduled_date", datetime.utcnow()),
188
+ "status": "draft",
189
+ "created_at": datetime.utcnow()
190
+ }
191
+
192
+ @app.get("/api/posts", response_model=List[PostResponse])
193
+ async def get_posts():
194
+ """Get list of posts"""
195
+ # Mock data for now
196
+ return [
197
+ {
198
+ "id": 1,
199
+ "title": "OCR Document Automation Benefits",
200
+ "content": "Transform your document processing...",
201
+ "post_type": "carousel",
202
+ "product_category": "ocr",
203
+ "scheduled_date": datetime.utcnow(),
204
+ "status": "scheduled",
205
+ "created_at": datetime.utcnow()
206
+ }
207
+ ]
208
+
209
+ # ---- Campaign Management ----
210
+
211
+ @app.post("/api/campaigns/generate")
212
+ async def generate_campaign(campaign_data: dict):
213
+ """Generate a campaign schedule using agentic AI"""
214
+ try:
215
+ # This would use AI to generate a schedule based on:
216
+ # - Date range
217
+ # - Products to focus on
218
+ # - Post types mix
219
+ # - Posts per week
220
+
221
+ # Mock implementation
222
+ return {
223
+ "campaign_id": 1,
224
+ "generated_posts": 12,
225
+ "schedule": []
226
+ }
227
+ except Exception as e:
228
+ raise HTTPException(status_code=500, detail=str(e))
229
 
230
  # ---- Frontend static serving ----
231
  FRONTEND_DIST = Path(__file__).resolve().parents[2] / "frontend" / "dist"
backend/app/models.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean, JSON, Text, ForeignKey
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import relationship
4
+ from datetime import datetime
5
+
6
+ Base = declarative_base()
7
+
8
+ class User(Base):
9
+ __tablename__ = "users"
10
+
11
+ id = Column(Integer, primary_key=True, index=True)
12
+ email = Column(String, unique=True, index=True)
13
+ name = Column(String)
14
+ created_at = Column(DateTime, default=datetime.utcnow)
15
+
16
+ integrations = relationship("Integration", back_populates="user")
17
+ assets = relationship("Asset", back_populates="user")
18
+ posts = relationship("Post", back_populates="user")
19
+
20
+ class Integration(Base):
21
+ __tablename__ = "integrations"
22
+
23
+ id = Column(Integer, primary_key=True, index=True)
24
+ user_id = Column(Integer, ForeignKey("users.id"))
25
+ provider = Column(String) # 'linkedin' or 'canva'
26
+ access_token = Column(Text)
27
+ refresh_token = Column(Text)
28
+ expires_at = Column(DateTime)
29
+ account_info = Column(JSON)
30
+ connected = Column(Boolean, default=False)
31
+ created_at = Column(DateTime, default=datetime.utcnow)
32
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
33
+
34
+ user = relationship("User", back_populates="integrations")
35
+
36
+ class Asset(Base):
37
+ __tablename__ = "assets"
38
+
39
+ id = Column(Integer, primary_key=True, index=True)
40
+ user_id = Column(Integer, ForeignKey("users.id"))
41
+ name = Column(String)
42
+ file_path = Column(String)
43
+ file_type = Column(String) # 'image', 'document', 'video'
44
+ product_category = Column(String) # 'ocr', 'p2p', 'o2c'
45
+ sub_category = Column(String, nullable=True)
46
+ size = Column(Integer) # in bytes
47
+ metadata = Column(JSON, nullable=True)
48
+ created_at = Column(DateTime, default=datetime.utcnow)
49
+
50
+ user = relationship("User", back_populates="assets")
51
+
52
+ class Post(Base):
53
+ __tablename__ = "posts"
54
+
55
+ id = Column(Integer, primary_key=True, index=True)
56
+ user_id = Column(Integer, ForeignKey("users.id"))
57
+ title = Column(String)
58
+ content = Column(Text)
59
+ post_type = Column(String) # 'carousel', 'cover_content', 'content_only', 'webinar'
60
+ product_category = Column(String)
61
+ scheduled_date = Column(DateTime)
62
+ status = Column(String) # 'draft', 'scheduled', 'published', 'failed'
63
+ linkedin_post_id = Column(String, nullable=True)
64
+ canva_design_id = Column(String, nullable=True)
65
+ assets = Column(JSON) # List of asset IDs
66
+ metadata = Column(JSON, nullable=True)
67
+ created_at = Column(DateTime, default=datetime.utcnow)
68
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
69
+
70
+ user = relationship("User", back_populates="posts")
71
+
72
+ class Campaign(Base):
73
+ __tablename__ = "campaigns"
74
+
75
+ id = Column(Integer, primary_key=True, index=True)
76
+ user_id = Column(Integer, ForeignKey("users.id"))
77
+ name = Column(String)
78
+ date_range_start = Column(DateTime)
79
+ date_range_end = Column(DateTime)
80
+ products = Column(JSON) # List of product IDs
81
+ post_types = Column(JSON) # List of post type IDs
82
+ posts_per_week = Column(Integer)
83
+ status = Column(String) # 'active', 'paused', 'completed'
84
+ created_at = Column(DateTime, default=datetime.utcnow)
85
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
86
+
backend/app/schemas.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from datetime import datetime
3
+ from typing import Optional, List, Dict, Any
4
+
5
+ class IntegrationCreate(BaseModel):
6
+ provider: str
7
+ access_token: str
8
+ refresh_token: Optional[str] = None
9
+ expires_at: Optional[datetime] = None
10
+ account_info: Optional[Dict[str, Any]] = None
11
+
12
+ class IntegrationResponse(BaseModel):
13
+ id: int
14
+ provider: str
15
+ connected: bool
16
+ account_info: Optional[Dict[str, Any]] = None
17
+ created_at: datetime
18
+
19
+ class Config:
20
+ from_attributes = True
21
+
22
+ class AssetCreate(BaseModel):
23
+ name: str
24
+ file_type: str
25
+ product_category: str
26
+ sub_category: Optional[str] = None
27
+ size: int
28
+ metadata: Optional[Dict[str, Any]] = None
29
+
30
+ class AssetResponse(BaseModel):
31
+ id: int
32
+ name: str
33
+ file_type: str
34
+ product_category: str
35
+ sub_category: Optional[str] = None
36
+ size: int
37
+ created_at: datetime
38
+
39
+ class Config:
40
+ from_attributes = True
41
+
42
+ class PostCreate(BaseModel):
43
+ title: str
44
+ content: str
45
+ post_type: str
46
+ product_category: str
47
+ scheduled_date: datetime
48
+ assets: Optional[List[int]] = None
49
+
50
+ class PostResponse(BaseModel):
51
+ id: int
52
+ title: str
53
+ content: str
54
+ post_type: str
55
+ product_category: str
56
+ scheduled_date: datetime
57
+ status: str
58
+ created_at: datetime
59
+
60
+ class Config:
61
+ from_attributes = True
62
+
63
+ class CampaignCreate(BaseModel):
64
+ name: str
65
+ date_range_start: datetime
66
+ date_range_end: datetime
67
+ products: List[str]
68
+ post_types: List[str]
69
+ posts_per_week: int
70
+
71
+ class CampaignResponse(BaseModel):
72
+ id: int
73
+ name: str
74
+ date_range_start: datetime
75
+ date_range_end: datetime
76
+ products: List[str]
77
+ post_types: List[str]
78
+ posts_per_week: int
79
+ status: str
80
+ created_at: datetime
81
+
82
+ class Config:
83
+ from_attributes = True
84
+
85
+ class CanvaBrandTemplate(BaseModel):
86
+ id: str
87
+ title: str
88
+ view_url: str
89
+ create_url: str
90
+ thumbnail: Optional[Dict[str, Any]] = None
91
+
92
+ class CanvaAutofillRequest(BaseModel):
93
+ brand_template_id: str
94
+ title: str
95
+ data: Dict[str, Any]
96
+
97
+ class CanvaAutofillResponse(BaseModel):
98
+ job_id: str
99
+ status: str
100
+
101
+ class LinkedInPostRequest(BaseModel):
102
+ text: str
103
+ media_uris: Optional[List[str]] = None
104
+ scheduled_time: Optional[datetime] = None
105
+
106
+ class AIContentRequest(BaseModel):
107
+ product_category: str
108
+ post_type: str
109
+ context: Optional[str] = None
110
+ assets: Optional[List[int]] = None
111
+
112
+ class AIContentResponse(BaseModel):
113
+ content: str
114
+ suggested_hashtags: List[str]
115
+
backend/app/services/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Services package
2
+
backend/app/services/ai_service.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import os
3
+ from typing import List, Dict, Any, Optional
4
+ from openai import OpenAI
5
+ from app.schemas import AIContentRequest, AIContentResponse
6
+
7
+ class AIService:
8
+ def __init__(self):
9
+ self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", ""))
10
+ self.model = os.getenv("OPENAI_MODEL", "gpt-4o")
11
+
12
+ async def generate_content(self, request: AIContentRequest, assets_context: Optional[str] = None) -> AIContentResponse:
13
+ """Generate LinkedIn post content using GPT"""
14
+
15
+ product_descriptions = {
16
+ "ocr": "Intelligent Document Parsing (OCR) - AI-powered document processing and data extraction",
17
+ "p2p": "Purchase To Pay (P2P) - End-to-end procurement and accounts payable automation",
18
+ "o2c": "Order to Cash (O2C) - Complete order management and accounts receivable workflow"
19
+ }
20
+
21
+ post_type_descriptions = {
22
+ "carousel": "A multi-slide carousel post with visual storytelling",
23
+ "cover_content": "A post with a cover image and engaging text content",
24
+ "content_only": "A text-only post focused on valuable insights",
25
+ "webinar": "A webinar invitation post to promote an upcoming event"
26
+ }
27
+
28
+ system_prompt = f"""You are an expert LinkedIn content creator specializing in B2B SaaS marketing.
29
+ Create engaging, professional LinkedIn posts that:
30
+ - Are authentic and valuable to the audience
31
+ - Include relevant hashtags (3-5 hashtags)
32
+ - Use emojis sparingly and appropriately
33
+ - Are optimized for engagement
34
+ - Follow LinkedIn best practices
35
+
36
+ Product: {product_descriptions.get(request.product_category, request.product_category)}
37
+ Post Type: {post_type_descriptions.get(request.post_type, request.post_type)}
38
+ """
39
+
40
+ user_prompt = f"""Create a LinkedIn post about {product_descriptions.get(request.product_category, request.product_category)}.
41
+ Post type: {post_type_descriptions.get(request.post_type, request.post_type)}
42
+
43
+ {f'Additional context: {request.context}' if request.context else ''}
44
+ {f'Available assets: {assets_context}' if assets_context else ''}
45
+
46
+ Make it engaging, professional, and include relevant hashtags at the end."""
47
+
48
+ try:
49
+ response = self.client.chat.completions.create(
50
+ model=self.model,
51
+ messages=[
52
+ {"role": "system", "content": system_prompt},
53
+ {"role": "user", "content": user_prompt}
54
+ ],
55
+ temperature=0.7,
56
+ max_tokens=1000
57
+ )
58
+
59
+ content = response.choices[0].message.content
60
+
61
+ # Extract hashtags
62
+ hashtags = []
63
+ lines = content.split('\n')
64
+ for line in lines:
65
+ if '#' in line:
66
+ hashtags.extend([tag.strip() for tag in line.split() if tag.startswith('#')])
67
+
68
+ # If no hashtags found, generate some
69
+ if not hashtags:
70
+ hashtags = self._generate_hashtags(request.product_category)
71
+
72
+ return AIContentResponse(
73
+ content=content,
74
+ suggested_hashtags=hashtags[:5]
75
+ )
76
+ except Exception as e:
77
+ # Fallback content if AI fails
78
+ return AIContentResponse(
79
+ content=f"🚀 Exciting news about {product_descriptions.get(request.product_category, 'our product')}!\n\nStay tuned for more updates.\n\n#Innovation #Technology",
80
+ suggested_hashtags=["#Innovation", "#Technology", "#Business"]
81
+ )
82
+
83
+ def _generate_hashtags(self, product_category: str) -> List[str]:
84
+ """Generate relevant hashtags based on product category"""
85
+ hashtag_map = {
86
+ "ocr": ["#DocumentAutomation", "#OCR", "#AITechnology", "#DigitalTransformation", "#BusinessEfficiency"],
87
+ "p2p": ["#Procurement", "#AccountsPayable", "#FinanceAutomation", "#BusinessProcess", "#Efficiency"],
88
+ "o2c": ["#OrderManagement", "#AccountsReceivable", "#SalesAutomation", "#BusinessGrowth", "#CustomerExperience"]
89
+ }
90
+ return hashtag_map.get(product_category, ["#Innovation", "#Technology", "#Business"])
91
+
backend/app/services/canva_service.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import os
3
+ from typing import List, Dict, Any, Optional
4
+ from app.schemas import CanvaBrandTemplate, CanvaAutofillRequest, CanvaAutofillResponse
5
+
6
+ CANVA_API_BASE = "https://api.canva.com/rest/v1"
7
+
8
+ class CanvaService:
9
+ def __init__(self, access_token: str):
10
+ self.access_token = access_token
11
+ self.headers = {
12
+ "Authorization": f"Bearer {access_token}",
13
+ "Content-Type": "application/json"
14
+ }
15
+
16
+ async def get_brand_templates(self, dataset: str = "non_empty") -> List[CanvaBrandTemplate]:
17
+ """Get list of Canva brand templates"""
18
+ async with httpx.AsyncClient() as client:
19
+ response = await client.get(
20
+ f"{CANVA_API_BASE}/brand-templates",
21
+ params={"dataset": dataset},
22
+ headers=self.headers,
23
+ timeout=30.0
24
+ )
25
+ response.raise_for_status()
26
+ data = response.json()
27
+
28
+ templates = []
29
+ for item in data.get("items", []):
30
+ templates.append(CanvaBrandTemplate(
31
+ id=item["id"],
32
+ title=item["title"],
33
+ view_url=item.get("view_url", ""),
34
+ create_url=item.get("create_url", ""),
35
+ thumbnail=item.get("thumbnail")
36
+ ))
37
+
38
+ return templates
39
+
40
+ async def get_brand_template_dataset(self, template_id: str) -> Dict[str, Any]:
41
+ """Get dataset for a specific brand template"""
42
+ async with httpx.AsyncClient() as client:
43
+ response = await client.get(
44
+ f"{CANVA_API_BASE}/brand-templates/{template_id}/dataset",
45
+ headers=self.headers,
46
+ timeout=30.0
47
+ )
48
+ response.raise_for_status()
49
+ return response.json()
50
+
51
+ async def create_autofill_job(self, request: CanvaAutofillRequest) -> CanvaAutofillResponse:
52
+ """Create an autofill job for a brand template"""
53
+ async with httpx.AsyncClient() as client:
54
+ response = await client.post(
55
+ f"{CANVA_API_BASE}/autofills",
56
+ headers=self.headers,
57
+ json={
58
+ "brand_template_id": request.brand_template_id,
59
+ "title": request.title,
60
+ "data": request.data
61
+ },
62
+ timeout=30.0
63
+ )
64
+ response.raise_for_status()
65
+ data = response.json()
66
+
67
+ return CanvaAutofillResponse(
68
+ job_id=data["job"]["id"],
69
+ status=data["job"]["status"]
70
+ )
71
+
72
+ async def get_autofill_job_status(self, job_id: str) -> Dict[str, Any]:
73
+ """Get status of an autofill job"""
74
+ async with httpx.AsyncClient() as client:
75
+ response = await client.get(
76
+ f"{CANVA_API_BASE}/autofills/{job_id}",
77
+ headers=self.headers,
78
+ timeout=30.0
79
+ )
80
+ response.raise_for_status()
81
+ return response.json()
82
+
backend/app/services/linkedin_service.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import os
3
+ from typing import Dict, Any, Optional
4
+ from datetime import datetime
5
+
6
+ LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
7
+
8
+ class LinkedInService:
9
+ def __init__(self, access_token: str):
10
+ self.access_token = access_token
11
+ self.headers = {
12
+ "Authorization": f"Bearer {access_token}",
13
+ "Content-Type": "application/json",
14
+ "X-Restli-Protocol-Version": "2.0.0"
15
+ }
16
+
17
+ async def get_user_profile(self) -> Dict[str, Any]:
18
+ """Get LinkedIn user profile"""
19
+ async with httpx.AsyncClient() as client:
20
+ response = await client.get(
21
+ f"{LINKEDIN_API_BASE}/userinfo",
22
+ headers=self.headers,
23
+ timeout=30.0
24
+ )
25
+ response.raise_for_status()
26
+ return response.json()
27
+
28
+ async def create_post(self, text: str, media_uris: Optional[list] = None) -> Dict[str, Any]:
29
+ """Create a LinkedIn post"""
30
+ # First, register upload if media is provided
31
+ media_urn = None
32
+ if media_uris:
33
+ # Register media upload
34
+ media_urn = await self._register_upload(media_uris[0])
35
+
36
+ # Create the post
37
+ post_data = {
38
+ "author": f"urn:li:person:{os.getenv('LINKEDIN_PERSON_URN', '')}",
39
+ "lifecycleState": "PUBLISHED",
40
+ "specificContent": {
41
+ "com.linkedin.ugc.ShareContent": {
42
+ "shareCommentary": {
43
+ "text": text
44
+ },
45
+ "shareMediaCategory": "IMAGE" if media_urn else "NONE"
46
+ }
47
+ },
48
+ "visibility": {
49
+ "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
50
+ }
51
+ }
52
+
53
+ if media_urn:
54
+ post_data["specificContent"]["com.linkedin.ugc.ShareContent"]["media"] = [
55
+ {
56
+ "status": "READY",
57
+ "media": media_urn
58
+ }
59
+ ]
60
+
61
+ async with httpx.AsyncClient() as client:
62
+ response = await client.post(
63
+ f"{LINKEDIN_API_BASE}/ugcPosts",
64
+ headers=self.headers,
65
+ json=post_data,
66
+ timeout=30.0
67
+ )
68
+ response.raise_for_status()
69
+ return response.json()
70
+
71
+ async def _register_upload(self, image_url: str) -> str:
72
+ """Register an image upload for LinkedIn"""
73
+ # This is a simplified version - actual implementation would upload the image
74
+ # and get a URN back
75
+ async with httpx.AsyncClient() as client:
76
+ # Register upload
77
+ register_response = await client.post(
78
+ f"{LINKEDIN_API_BASE}/assets?action=registerUpload",
79
+ headers=self.headers,
80
+ json={
81
+ "registerUploadRequest": {
82
+ "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
83
+ "owner": f"urn:li:person:{os.getenv('LINKEDIN_PERSON_URN', '')}",
84
+ "serviceRelationships": [
85
+ {
86
+ "relationshipType": "OWNER",
87
+ "identifier": "urn:li:userGeneratedContent"
88
+ }
89
+ ]
90
+ }
91
+ },
92
+ timeout=30.0
93
+ )
94
+ register_response.raise_for_status()
95
+ register_data = register_response.json()
96
+
97
+ # Upload the image
98
+ upload_url = register_data["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"]
99
+ asset_urn = register_data["value"]["asset"]
100
+
101
+ # Download and upload the image
102
+ image_response = await client.get(image_url, timeout=30.0)
103
+ image_response.raise_for_status()
104
+
105
+ await client.put(
106
+ upload_url,
107
+ content=image_response.content,
108
+ headers={"Content-Type": "application/octet-stream"},
109
+ timeout=30.0
110
+ )
111
+
112
+ return asset_urn
113
+
backend/requirements.txt CHANGED
@@ -1,2 +1,13 @@
1
  fastapi
2
- uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
1
  fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ aiofiles
5
+ httpx
6
+ openai
7
+ python-dotenv
8
+ pydantic
9
+ python-jose[cryptography]
10
+ passlib[bcrypt]
11
+ sqlalchemy
12
+ alembic
13
+ psycopg2-binary
frontend/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>HF React + FastAPI by Seth</title>
7
  </head>
8
  <body>
9
  <div id="root"></div>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>PostGen - LinkedIn Content Scheduler</title>
7
  </head>
8
  <body>
9
  <div id="root"></div>
frontend/package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "hf-react",
3
  "private": true,
4
  "version": "0.0.1",
5
  "type": "module",
@@ -10,10 +10,35 @@
10
  },
11
  "dependencies": {
12
  "react": "^18.3.1",
13
- "react-dom": "^18.3.1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  },
15
  "devDependencies": {
16
  "@vitejs/plugin-react": "^4.3.1",
17
- "vite": "^5.4.2"
 
 
 
18
  }
19
  }
 
1
  {
2
+ "name": "postgen",
3
  "private": true,
4
  "version": "0.0.1",
5
  "type": "module",
 
10
  },
11
  "dependencies": {
12
  "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "react-router-dom": "^6.26.0",
15
+ "framer-motion": "^11.0.0",
16
+ "lucide-react": "^0.344.0",
17
+ "date-fns": "^3.3.1",
18
+ "clsx": "^2.1.0",
19
+ "tailwind-merge": "^2.2.1",
20
+ "@radix-ui/react-dialog": "^1.0.5",
21
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
22
+ "@radix-ui/react-label": "^2.0.2",
23
+ "@radix-ui/react-popover": "^1.0.7",
24
+ "@radix-ui/react-select": "^2.0.0",
25
+ "@radix-ui/react-separator": "^1.0.3",
26
+ "@radix-ui/react-slot": "^1.0.2",
27
+ "@radix-ui/react-switch": "^1.0.3",
28
+ "@radix-ui/react-tabs": "^1.0.4",
29
+ "@radix-ui/react-avatar": "^1.0.4",
30
+ "@radix-ui/react-checkbox": "^1.0.4",
31
+ "@radix-ui/react-slider": "^1.1.2",
32
+ "@radix-ui/react-alert-dialog": "^1.0.5",
33
+ "@radix-ui/react-progress": "^1.0.3",
34
+ "class-variance-authority": "^0.7.0",
35
+ "react-day-picker": "^8.10.0"
36
  },
37
  "devDependencies": {
38
  "@vitejs/plugin-react": "^4.3.1",
39
+ "vite": "^5.4.2",
40
+ "autoprefixer": "^10.4.17",
41
+ "postcss": "^8.4.33",
42
+ "tailwindcss": "^3.4.1"
43
  }
44
  }
frontend/postcss.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
frontend/src/App.jsx CHANGED
@@ -1,23 +1,25 @@
1
- import React, { useEffect, useState } from "react";
2
-
3
- export default function App() {
4
- const [apiMsg, setApiMsg] = useState("");
5
-
6
- useEffect(() => {
7
- fetch("/api/hello")
8
- .then((r) => r.json())
9
- .then((d) => setApiMsg(d.message))
10
- .catch(() => setApiMsg("API not reachable yet"));
11
- }, []);
12
 
 
13
  return (
14
- <div style={{ fontFamily: "system-ui", padding: 24, lineHeight: 1.5 }}>
15
- <h1>React + FastAPI (Docker, HF Spaces)</h1>
16
- <p>This is a plain starter page. Customize freely.By Seth</p>
17
-
18
- <div style={{ marginTop: 16, padding: 12, border: "1px solid #ddd", borderRadius: 8 }}>
19
- <strong>API says:</strong> {apiMsg}
20
- </div>
21
- </div>
 
22
  );
23
  }
 
 
 
1
+ import React from "react";
2
+ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3
+ import Layout from "./components/Layout";
4
+ import Dashboard from "./pages/Dashboard";
5
+ import Repository from "./pages/Repository";
6
+ import Scheduler from "./pages/Scheduler";
7
+ import PostEditor from "./pages/PostEditor";
8
+ import Integrations from "./pages/Integrations";
9
+ import { createPageUrl } from "./utils";
 
 
10
 
11
+ function App() {
12
  return (
13
+ <Router>
14
+ <Routes>
15
+ <Route path="/" element={<Layout currentPageName="Dashboard"><Dashboard /></Layout>} />
16
+ <Route path={createPageUrl("Repository")} element={<Layout currentPageName="Repository"><Repository /></Layout>} />
17
+ <Route path={createPageUrl("Scheduler")} element={<Layout currentPageName="Scheduler"><Scheduler /></Layout>} />
18
+ <Route path={createPageUrl("PostEditor")} element={<Layout currentPageName="PostEditor"><PostEditor /></Layout>} />
19
+ <Route path={createPageUrl("Integrations")} element={<Layout currentPageName="Integrations"><Integrations /></Layout>} />
20
+ </Routes>
21
+ </Router>
22
  );
23
  }
24
+
25
+ export default App;
frontend/src/components/Layout.jsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { createPageUrl } from '@/utils';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import {
6
+ LayoutDashboard,
7
+ FolderOpen,
8
+ Calendar,
9
+ PenTool,
10
+ Link2,
11
+ Settings,
12
+ Menu,
13
+ X,
14
+ Bell,
15
+ Search,
16
+ ChevronDown,
17
+ Linkedin,
18
+ Sparkles,
19
+ LogOut,
20
+ User,
21
+ HelpCircle
22
+ } from 'lucide-react';
23
+ import { Button } from '@/components/ui/button';
24
+ import { Input } from '@/components/ui/input';
25
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
26
+ import { Badge } from '@/components/ui/badge';
27
+ import {
28
+ DropdownMenu,
29
+ DropdownMenuContent,
30
+ DropdownMenuItem,
31
+ DropdownMenuLabel,
32
+ DropdownMenuSeparator,
33
+ DropdownMenuTrigger,
34
+ } from '@/components/ui/dropdown-menu';
35
+
36
+ const navItems = [
37
+ { name: 'Dashboard', icon: LayoutDashboard, href: 'Dashboard' },
38
+ { name: 'Repository', icon: FolderOpen, href: 'Repository' },
39
+ { name: 'Scheduler', icon: Calendar, href: 'Scheduler' },
40
+ { name: 'Post Editor', icon: PenTool, href: 'PostEditor' },
41
+ { name: 'Integrations', icon: Link2, href: 'Integrations' },
42
+ ];
43
+
44
+ export default function Layout({ children, currentPageName }) {
45
+ const [sidebarOpen, setSidebarOpen] = useState(false);
46
+ const location = useLocation();
47
+ const currentPage = navItems.find(item => createPageUrl(item.href) === location.pathname)?.href || 'Dashboard';
48
+
49
+ return (
50
+ <div className="min-h-screen bg-slate-50">
51
+ {/* Mobile Sidebar Overlay */}
52
+ <AnimatePresence>
53
+ {sidebarOpen && (
54
+ <motion.div
55
+ initial={{ opacity: 0 }}
56
+ animate={{ opacity: 1 }}
57
+ exit={{ opacity: 0 }}
58
+ className="fixed inset-0 bg-black/50 z-40 lg:hidden"
59
+ onClick={() => setSidebarOpen(false)}
60
+ />
61
+ )}
62
+ </AnimatePresence>
63
+
64
+ {/* Sidebar */}
65
+ <aside className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-slate-200 transform transition-transform duration-300 ease-in-out lg:translate-x-0 ${
66
+ sidebarOpen ? 'translate-x-0' : '-translate-x-full'
67
+ }`}>
68
+ <div className="flex flex-col h-full">
69
+ {/* Logo */}
70
+ <div className="h-16 flex items-center justify-between px-4 border-b border-slate-100">
71
+ <Link to={createPageUrl('Dashboard')} className="flex items-center gap-2">
72
+ <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/30">
73
+ <Linkedin className="w-5 h-5 text-white" />
74
+ </div>
75
+ <div>
76
+ <span className="font-bold text-slate-900 text-lg">PostGen</span>
77
+ <span className="text-xs text-slate-500 block -mt-1">LinkedIn Scheduler</span>
78
+ </div>
79
+ </Link>
80
+ <Button
81
+ variant="ghost"
82
+ size="icon"
83
+ className="lg:hidden"
84
+ onClick={() => setSidebarOpen(false)}
85
+ >
86
+ <X className="w-5 h-5" />
87
+ </Button>
88
+ </div>
89
+
90
+ {/* Navigation */}
91
+ <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
92
+ {navItems.map((item) => {
93
+ const isActive = currentPage === item.href;
94
+ return (
95
+ <Link
96
+ key={item.name}
97
+ to={createPageUrl(item.href)}
98
+ onClick={() => setSidebarOpen(false)}
99
+ className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${
100
+ isActive
101
+ ? 'bg-blue-50 text-blue-700'
102
+ : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
103
+ }`}
104
+ >
105
+ <item.icon className={`w-5 h-5 ${isActive ? 'text-blue-600' : 'text-slate-400'}`} />
106
+ {item.name}
107
+ {item.name === 'Repository' && (
108
+ <Badge className="ml-auto bg-blue-100 text-blue-700 text-[10px] px-1.5 py-0 border-0">
109
+ 156
110
+ </Badge>
111
+ )}
112
+ </Link>
113
+ );
114
+ })}
115
+ </nav>
116
+
117
+ {/* Quick Create */}
118
+ <div className="p-4 border-t border-slate-100">
119
+ <Link to={createPageUrl('PostEditor')}>
120
+ <Button className="w-full gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
121
+ <Sparkles className="w-4 h-4" />
122
+ Create Post
123
+ </Button>
124
+ </Link>
125
+ </div>
126
+
127
+ {/* User Profile */}
128
+ <div className="p-4 border-t border-slate-100">
129
+ <DropdownMenu>
130
+ <DropdownMenuTrigger asChild>
131
+ <button className="flex items-center gap-3 w-full p-2 rounded-xl hover:bg-slate-50 transition-colors">
132
+ <Avatar className="h-9 w-9">
133
+ <AvatarImage src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop" />
134
+ <AvatarFallback>AB</AvatarFallback>
135
+ </Avatar>
136
+ <div className="flex-1 text-left">
137
+ <p className="text-sm font-medium text-slate-900">Alex Business</p>
138
+ <p className="text-xs text-slate-500">Pro Plan</p>
139
+ </div>
140
+ <ChevronDown className="w-4 h-4 text-slate-400" />
141
+ </button>
142
+ </DropdownMenuTrigger>
143
+ <DropdownMenuContent align="end" className="w-56">
144
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
145
+ <DropdownMenuSeparator />
146
+ <DropdownMenuItem>
147
+ <User className="w-4 h-4 mr-2" />
148
+ Profile Settings
149
+ </DropdownMenuItem>
150
+ <DropdownMenuItem>
151
+ <Settings className="w-4 h-4 mr-2" />
152
+ Preferences
153
+ </DropdownMenuItem>
154
+ <DropdownMenuItem>
155
+ <HelpCircle className="w-4 h-4 mr-2" />
156
+ Help & Support
157
+ </DropdownMenuItem>
158
+ <DropdownMenuSeparator />
159
+ <DropdownMenuItem className="text-red-600">
160
+ <LogOut className="w-4 h-4 mr-2" />
161
+ Sign Out
162
+ </DropdownMenuItem>
163
+ </DropdownMenuContent>
164
+ </DropdownMenu>
165
+ </div>
166
+ </div>
167
+ </aside>
168
+
169
+ {/* Main Content */}
170
+ <div className="lg:pl-64">
171
+ {/* Top Header */}
172
+ <header className="h-16 bg-white border-b border-slate-200 sticky top-0 z-30">
173
+ <div className="h-full px-4 flex items-center justify-between gap-4">
174
+ {/* Mobile Menu Toggle */}
175
+ <Button
176
+ variant="ghost"
177
+ size="icon"
178
+ className="lg:hidden"
179
+ onClick={() => setSidebarOpen(true)}
180
+ >
181
+ <Menu className="w-5 h-5" />
182
+ </Button>
183
+
184
+ {/* Search */}
185
+ <div className="flex-1 max-w-md hidden sm:block">
186
+ <div className="relative">
187
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
188
+ <Input
189
+ placeholder="Search posts, assets, schedules..."
190
+ className="pl-10 bg-slate-50 border-slate-200 focus:bg-white"
191
+ />
192
+ </div>
193
+ </div>
194
+
195
+ {/* Right Actions */}
196
+ <div className="flex items-center gap-2">
197
+ <Button variant="ghost" size="icon" className="relative">
198
+ <Bell className="w-5 h-5 text-slate-600" />
199
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full" />
200
+ </Button>
201
+
202
+ {/* Connection Status */}
203
+ <div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-emerald-50 border border-emerald-100">
204
+ <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
205
+ <span className="text-xs font-medium text-emerald-700">LinkedIn Connected</span>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </header>
210
+
211
+ {/* Page Content */}
212
+ <main>
213
+ {children}
214
+ </main>
215
+ </div>
216
+ </div>
217
+ );
218
+ }
219
+
frontend/src/components/ui/alert.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "@/utils";
4
+
5
+ const alertVariants = cva(
6
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-background text-foreground",
11
+ destructive:
12
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ }
19
+ );
20
+
21
+ const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
22
+ <div
23
+ ref={ref}
24
+ role="alert"
25
+ className={cn(alertVariants({ variant }), className)}
26
+ {...props}
27
+ />
28
+ ));
29
+ Alert.displayName = "Alert";
30
+
31
+ const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
32
+ <h5
33
+ ref={ref}
34
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
35
+ {...props}
36
+ />
37
+ ));
38
+ AlertTitle.displayName = "AlertTitle";
39
+
40
+ const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
41
+ <div
42
+ ref={ref}
43
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
44
+ {...props}
45
+ />
46
+ ));
47
+ AlertDescription.displayName = "AlertDescription";
48
+
49
+ export { Alert, AlertTitle, AlertDescription };
50
+
frontend/src/components/ui/avatar.jsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+ import { cn } from "@/utils";
4
+
5
+ const Avatar = React.forwardRef(({ className, ...props }, ref) => (
6
+ <AvatarPrimitive.Root
7
+ ref={ref}
8
+ className={cn(
9
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ ));
15
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
16
+
17
+ const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
18
+ <AvatarPrimitive.Image
19
+ ref={ref}
20
+ className={cn("aspect-square h-full w-full", className)}
21
+ {...props}
22
+ />
23
+ ));
24
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
25
+
26
+ const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Fallback
28
+ ref={ref}
29
+ className={cn(
30
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ ));
36
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
37
+
38
+ export { Avatar, AvatarImage, AvatarFallback };
39
+
frontend/src/components/ui/badge.jsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "@/utils";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ }
23
+ );
24
+
25
+ function Badge({ className, variant, ...props }) {
26
+ return (
27
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
28
+ );
29
+ }
30
+
31
+ export { Badge, badgeVariants };
32
+
frontend/src/components/ui/button.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva } from "class-variance-authority";
4
+ import { cn } from "@/utils";
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ destructive:
13
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
+ ghost: "hover:bg-accent hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-10 px-4 py-2",
23
+ sm: "h-9 rounded-md px-3",
24
+ lg: "h-11 rounded-md px-8",
25
+ icon: "h-10 w-10",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ }
33
+ );
34
+
35
+ const Button = React.forwardRef(
36
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
37
+ const Comp = asChild ? Slot : "button";
38
+ return (
39
+ <Comp
40
+ className={cn(buttonVariants({ variant, size, className }))}
41
+ ref={ref}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+ );
47
+ Button.displayName = "Button";
48
+
49
+ export { Button, buttonVariants };
50
+
frontend/src/components/ui/calendar.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { DayPicker } from "react-day-picker";
4
+ import { cn } from "@/utils";
5
+
6
+ export function Calendar({ className, classNames, showOutsideDays = true, mode = "single", selected, onSelect, ...props }) {
7
+ return (
8
+ <DayPicker
9
+ mode={mode}
10
+ selected={selected}
11
+ onSelect={onSelect}
12
+ showOutsideDays={showOutsideDays}
13
+ className={cn("p-3", className)}
14
+ classNames={{
15
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
16
+ month: "space-y-4",
17
+ caption: "flex justify-center pt-1 relative items-center",
18
+ caption_label: "text-sm font-medium",
19
+ nav: "space-x-1 flex items-center",
20
+ nav_button: cn(
21
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
22
+ ),
23
+ nav_button_previous: "absolute left-1",
24
+ nav_button_next: "absolute right-1",
25
+ table: "w-full border-collapse space-y-1",
26
+ head_row: "flex",
27
+ head_cell:
28
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
29
+ row: "flex w-full mt-2",
30
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
31
+ day: cn(
32
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
33
+ ),
34
+ day_range_end: "day-range-end",
35
+ day_selected:
36
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
37
+ day_today: "bg-accent text-accent-foreground",
38
+ day_outside:
39
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
40
+ day_disabled: "text-muted-foreground opacity-50",
41
+ day_range_middle:
42
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
43
+ day_hidden: "invisible",
44
+ ...classNames,
45
+ }}
46
+ components={{
47
+ IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
48
+ IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
49
+ }}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
frontend/src/components/ui/card.jsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cn } from "@/utils";
3
+
4
+ const Card = React.forwardRef(({ className, ...props }, ref) => (
5
+ <div
6
+ ref={ref}
7
+ className={cn(
8
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
9
+ className
10
+ )}
11
+ {...props}
12
+ />
13
+ ));
14
+ Card.displayName = "Card";
15
+
16
+ const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
17
+ <div
18
+ ref={ref}
19
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
20
+ {...props}
21
+ />
22
+ ));
23
+ CardHeader.displayName = "CardHeader";
24
+
25
+ const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
26
+ <h3
27
+ ref={ref}
28
+ className={cn(
29
+ "text-2xl font-semibold leading-none tracking-tight",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ ));
35
+ CardTitle.displayName = "CardTitle";
36
+
37
+ const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
38
+ <p
39
+ ref={ref}
40
+ className={cn("text-sm text-muted-foreground", className)}
41
+ {...props}
42
+ />
43
+ ));
44
+ CardDescription.displayName = "CardDescription";
45
+
46
+ const CardContent = React.forwardRef(({ className, ...props }, ref) => (
47
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
48
+ ));
49
+ CardContent.displayName = "CardContent";
50
+
51
+ const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
52
+ <div
53
+ ref={ref}
54
+ className={cn("flex items-center p-6 pt-0", className)}
55
+ {...props}
56
+ />
57
+ ));
58
+ CardFooter.displayName = "CardFooter";
59
+
60
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
61
+
frontend/src/components/ui/checkbox.jsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+ import { cn } from "@/utils";
5
+
6
+ const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
7
+ <CheckboxPrimitive.Root
8
+ ref={ref}
9
+ className={cn(
10
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
11
+ className
12
+ )}
13
+ {...props}
14
+ >
15
+ <CheckboxPrimitive.Indicator
16
+ className={cn("flex items-center justify-center text-current")}
17
+ >
18
+ <Check className="h-4 w-4" />
19
+ </CheckboxPrimitive.Indicator>
20
+ </CheckboxPrimitive.Root>
21
+ ));
22
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
23
+
24
+ export { Checkbox };
25
+
frontend/src/components/ui/dialog.jsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { X } from "lucide-react";
4
+ import { cn } from "@/utils";
5
+
6
+ const Dialog = DialogPrimitive.Root;
7
+ const DialogTrigger = DialogPrimitive.Trigger;
8
+ const DialogPortal = DialogPrimitive.Portal;
9
+ const DialogClose = DialogPrimitive.Close;
10
+
11
+ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
12
+ <DialogPrimitive.Overlay
13
+ ref={ref}
14
+ className={cn(
15
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
22
+
23
+ const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
24
+ <DialogPortal>
25
+ <DialogOverlay />
26
+ <DialogPrimitive.Content
27
+ ref={ref}
28
+ className={cn(
29
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
36
+ <X className="h-4 w-4" />
37
+ <span className="sr-only">Close</span>
38
+ </DialogPrimitive.Close>
39
+ </DialogPrimitive.Content>
40
+ </DialogPortal>
41
+ ));
42
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
43
+
44
+ const DialogHeader = ({ className, ...props }) => (
45
+ <div
46
+ className={cn(
47
+ "flex flex-col space-y-1.5 text-center sm:text-left",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ DialogHeader.displayName = "DialogHeader";
54
+
55
+ const DialogFooter = ({ className, ...props }) => (
56
+ <div
57
+ className={cn(
58
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ DialogFooter.displayName = "DialogFooter";
65
+
66
+ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
67
+ <DialogPrimitive.Title
68
+ ref={ref}
69
+ className={cn(
70
+ "text-lg font-semibold leading-none tracking-tight",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ ));
76
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
77
+
78
+ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
79
+ <DialogPrimitive.Description
80
+ ref={ref}
81
+ className={cn("text-sm text-muted-foreground", className)}
82
+ {...props}
83
+ />
84
+ ));
85
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
86
+
87
+ export {
88
+ Dialog,
89
+ DialogPortal,
90
+ DialogOverlay,
91
+ DialogClose,
92
+ DialogTrigger,
93
+ DialogContent,
94
+ DialogHeader,
95
+ DialogFooter,
96
+ DialogTitle,
97
+ DialogDescription,
98
+ };
99
+
frontend/src/components/ui/dropdown-menu.jsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+ import { cn } from "@/utils";
5
+
6
+ const DropdownMenu = DropdownMenuPrimitive.Root;
7
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
8
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
9
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
10
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
11
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
12
+
13
+ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
14
+ <DropdownMenuPrimitive.SubTrigger
15
+ ref={ref}
16
+ className={cn(
17
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
18
+ inset && "pl-8",
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ <ChevronRight className="ml-auto h-4 w-4" />
25
+ </DropdownMenuPrimitive.SubTrigger>
26
+ ));
27
+ DropdownMenuSubTrigger.displayName =
28
+ DropdownMenuPrimitive.SubTrigger.displayName;
29
+
30
+ const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
31
+ <DropdownMenuPrimitive.SubContent
32
+ ref={ref}
33
+ className={cn(
34
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ ));
40
+ DropdownMenuSubContent.displayName =
41
+ DropdownMenuPrimitive.SubContent.displayName;
42
+
43
+ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
44
+ <DropdownMenuPrimitive.Portal>
45
+ <DropdownMenuPrimitive.Content
46
+ ref={ref}
47
+ sideOffset={sideOffset}
48
+ className={cn(
49
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
50
+ className
51
+ )}
52
+ {...props}
53
+ />
54
+ </DropdownMenuPrimitive.Portal>
55
+ ));
56
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
57
+
58
+ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
59
+ <DropdownMenuPrimitive.Item
60
+ ref={ref}
61
+ className={cn(
62
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
63
+ inset && "pl-8",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ ));
69
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
70
+
71
+ const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
72
+ <DropdownMenuPrimitive.CheckboxItem
73
+ ref={ref}
74
+ className={cn(
75
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
76
+ className
77
+ )}
78
+ checked={checked}
79
+ {...props}
80
+ >
81
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
82
+ <DropdownMenuPrimitive.ItemIndicator>
83
+ <Check className="h-4 w-4" />
84
+ </DropdownMenuPrimitive.ItemIndicator>
85
+ </span>
86
+ {children}
87
+ </DropdownMenuPrimitive.CheckboxItem>
88
+ ));
89
+ DropdownMenuCheckboxItem.displayName =
90
+ DropdownMenuPrimitive.CheckboxItem.displayName;
91
+
92
+ const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
93
+ <DropdownMenuPrimitive.RadioItem
94
+ ref={ref}
95
+ className={cn(
96
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
97
+ className
98
+ )}
99
+ {...props}
100
+ >
101
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <Circle className="h-2 w-2 fill-current" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.RadioItem>
108
+ ));
109
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
110
+
111
+ const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
112
+ <DropdownMenuPrimitive.Label
113
+ ref={ref}
114
+ className={cn(
115
+ "px-2 py-1.5 text-sm font-semibold",
116
+ inset && "pl-8",
117
+ className
118
+ )}
119
+ {...props}
120
+ />
121
+ ));
122
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
123
+
124
+ const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
125
+ <DropdownMenuPrimitive.Separator
126
+ ref={ref}
127
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
128
+ {...props}
129
+ />
130
+ ));
131
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
132
+
133
+ const DropdownMenuShortcut = ({ className, ...props }) => {
134
+ return (
135
+ <span
136
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ };
141
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
142
+
143
+ export {
144
+ DropdownMenu,
145
+ DropdownMenuTrigger,
146
+ DropdownMenuContent,
147
+ DropdownMenuItem,
148
+ DropdownMenuCheckboxItem,
149
+ DropdownMenuRadioItem,
150
+ DropdownMenuLabel,
151
+ DropdownMenuSeparator,
152
+ DropdownMenuShortcut,
153
+ DropdownMenuGroup,
154
+ DropdownMenuPortal,
155
+ DropdownMenuSub,
156
+ DropdownMenuSubContent,
157
+ DropdownMenuSubTrigger,
158
+ DropdownMenuRadioGroup,
159
+ };
160
+
frontend/src/components/ui/input.jsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cn } from "@/utils";
3
+
4
+ const Input = React.forwardRef(({ className, type, ...props }, ref) => {
5
+ return (
6
+ <input
7
+ type={type}
8
+ className={cn(
9
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
10
+ className
11
+ )}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ );
16
+ });
17
+ Input.displayName = "Input";
18
+
19
+ export { Input };
20
+
frontend/src/components/ui/label.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { cn } from "@/utils";
4
+
5
+ const Label = React.forwardRef(({ className, ...props }, ref) => (
6
+ <LabelPrimitive.Root
7
+ ref={ref}
8
+ className={cn(
9
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ ));
15
+ Label.displayName = LabelPrimitive.Root.displayName;
16
+
17
+ export { Label };
18
+
frontend/src/components/ui/popover.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { cn } from "@/utils";
4
+
5
+ const Popover = PopoverPrimitive.Root;
6
+ const PopoverTrigger = PopoverPrimitive.Trigger;
7
+ const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
8
+ <PopoverPrimitive.Portal>
9
+ <PopoverPrimitive.Content
10
+ ref={ref}
11
+ align={align}
12
+ sideOffset={sideOffset}
13
+ className={cn(
14
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ </PopoverPrimitive.Portal>
20
+ ));
21
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
22
+
23
+ export { Popover, PopoverTrigger, PopoverContent };
24
+
frontend/src/components/ui/progress.jsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
3
+ import { cn } from "@/utils";
4
+
5
+ const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
6
+ <ProgressPrimitive.Root
7
+ ref={ref}
8
+ className={cn(
9
+ "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
10
+ className
11
+ )}
12
+ {...props}
13
+ >
14
+ <ProgressPrimitive.Indicator
15
+ className="h-full w-full flex-1 bg-primary transition-all"
16
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
17
+ />
18
+ </ProgressPrimitive.Root>
19
+ ));
20
+ Progress.displayName = ProgressPrimitive.Root.displayName;
21
+
22
+ export { Progress };
23
+
frontend/src/components/ui/select.jsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as SelectPrimitive from "@radix-ui/react-select";
3
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
4
+ import { cn } from "@/utils";
5
+
6
+ const Select = SelectPrimitive.Root;
7
+ const SelectGroup = SelectPrimitive.Group;
8
+ const SelectValue = SelectPrimitive.Value;
9
+
10
+ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
11
+ <SelectPrimitive.Trigger
12
+ ref={ref}
13
+ className={cn(
14
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ {children}
20
+ <SelectPrimitive.Icon asChild>
21
+ <ChevronDown className="h-4 w-4 opacity-50" />
22
+ </SelectPrimitive.Icon>
23
+ </SelectPrimitive.Trigger>
24
+ ));
25
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
26
+
27
+ const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
28
+ <SelectPrimitive.ScrollUpButton
29
+ ref={ref}
30
+ className={cn(
31
+ "flex cursor-default items-center justify-center py-1",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ <ChevronUp className="h-4 w-4" />
37
+ </SelectPrimitive.ScrollUpButton>
38
+ ));
39
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
40
+
41
+ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
42
+ <SelectPrimitive.ScrollDownButton
43
+ ref={ref}
44
+ className={cn(
45
+ "flex cursor-default items-center justify-center py-1",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ <ChevronDown className="h-4 w-4" />
51
+ </SelectPrimitive.ScrollDownButton>
52
+ ));
53
+ SelectScrollDownButton.displayName =
54
+ SelectPrimitive.ScrollDownButton.displayName;
55
+
56
+ const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
57
+ <SelectPrimitive.Portal>
58
+ <SelectPrimitive.Content
59
+ ref={ref}
60
+ className={cn(
61
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
62
+ position === "popper" &&
63
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
64
+ className
65
+ )}
66
+ position={position}
67
+ {...props}
68
+ >
69
+ <SelectScrollUpButton />
70
+ <SelectPrimitive.Viewport
71
+ className={cn(
72
+ "p-1",
73
+ position === "popper" &&
74
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
75
+ )}
76
+ >
77
+ {children}
78
+ </SelectPrimitive.Viewport>
79
+ <SelectScrollDownButton />
80
+ </SelectPrimitive.Content>
81
+ </SelectPrimitive.Portal>
82
+ ));
83
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
84
+
85
+ const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
86
+ <SelectPrimitive.Label
87
+ ref={ref}
88
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
89
+ {...props}
90
+ />
91
+ ));
92
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
93
+
94
+ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
95
+ <SelectPrimitive.Item
96
+ ref={ref}
97
+ className={cn(
98
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
99
+ className
100
+ )}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <SelectPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </SelectPrimitive.ItemIndicator>
107
+ </span>
108
+
109
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
110
+ </SelectPrimitive.Item>
111
+ ));
112
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
113
+
114
+ const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
115
+ <SelectPrimitive.Separator
116
+ ref={ref}
117
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
118
+ {...props}
119
+ />
120
+ ));
121
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
122
+
123
+ export {
124
+ Select,
125
+ SelectGroup,
126
+ SelectValue,
127
+ SelectTrigger,
128
+ SelectContent,
129
+ SelectLabel,
130
+ SelectItem,
131
+ SelectSeparator,
132
+ SelectScrollUpButton,
133
+ SelectScrollDownButton,
134
+ };
135
+
frontend/src/components/ui/separator.jsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
3
+ import { cn } from "@/utils";
4
+
5
+ const Separator = React.forwardRef(
6
+ (
7
+ { className, orientation = "horizontal", decorative = true, ...props },
8
+ ref
9
+ ) => (
10
+ <SeparatorPrimitive.Root
11
+ ref={ref}
12
+ decorative={decorative}
13
+ orientation={orientation}
14
+ className={cn(
15
+ "shrink-0 bg-border",
16
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ );
23
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
24
+
25
+ export { Separator };
26
+
frontend/src/components/ui/slider.jsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as SliderPrimitive from "@radix-ui/react-slider";
3
+ import { cn } from "@/utils";
4
+
5
+ const Slider = React.forwardRef(({ className, ...props }, ref) => (
6
+ <SliderPrimitive.Root
7
+ ref={ref}
8
+ className={cn(
9
+ "relative flex w-full touch-none select-none items-center",
10
+ className
11
+ )}
12
+ {...props}
13
+ >
14
+ <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
15
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
16
+ </SliderPrimitive.Track>
17
+ <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
18
+ </SliderPrimitive.Root>
19
+ ));
20
+ Slider.displayName = SliderPrimitive.Root.displayName;
21
+
22
+ export { Slider };
23
+
frontend/src/components/ui/switch.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as SwitchPrimitives from "@radix-ui/react-switch";
3
+ import { cn } from "@/utils";
4
+
5
+ const Switch = React.forwardRef(({ className, ...props }, ref) => (
6
+ <SwitchPrimitives.Root
7
+ className={cn(
8
+ "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
9
+ className
10
+ )}
11
+ {...props}
12
+ ref={ref}
13
+ >
14
+ <SwitchPrimitives.Thumb
15
+ className={cn(
16
+ "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
17
+ )}
18
+ />
19
+ </SwitchPrimitives.Root>
20
+ ));
21
+ Switch.displayName = SwitchPrimitives.Root.displayName;
22
+
23
+ export { Switch };
24
+
frontend/src/components/ui/tabs.jsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
3
+ import { cn } from "@/utils";
4
+
5
+ const Tabs = TabsPrimitive.Root;
6
+ const TabsList = React.forwardRef(({ className, ...props }, ref) => (
7
+ <TabsPrimitive.List
8
+ ref={ref}
9
+ className={cn(
10
+ "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ ));
16
+ TabsList.displayName = TabsPrimitive.List.displayName;
17
+
18
+ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
19
+ <TabsPrimitive.Trigger
20
+ ref={ref}
21
+ className={cn(
22
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ));
28
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
29
+
30
+ const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
31
+ <TabsPrimitive.Content
32
+ ref={ref}
33
+ className={cn(
34
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ ));
40
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
41
+
42
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
43
+
frontend/src/components/ui/textarea.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cn } from "@/utils";
3
+
4
+ const Textarea = React.forwardRef(({ className, ...props }, ref) => {
5
+ return (
6
+ <textarea
7
+ className={cn(
8
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
9
+ className
10
+ )}
11
+ ref={ref}
12
+ {...props}
13
+ />
14
+ );
15
+ });
16
+ Textarea.displayName = "Textarea";
17
+
18
+ export { Textarea };
19
+
frontend/src/index.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 221.2 83.2% 53.3%;
26
+ --radius: 0.5rem;
27
+ }
28
+ }
29
+
30
+ @layer base {
31
+ * {
32
+ @apply border-border;
33
+ }
34
+ body {
35
+ @apply bg-background text-foreground;
36
+ }
37
+ }
38
+
frontend/src/main.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import React from "react";
2
  import ReactDOM from "react-dom/client";
3
  import App from "./App.jsx";
 
4
 
5
  ReactDOM.createRoot(document.getElementById("root")).render(
6
  <React.StrictMode>
 
1
  import React from "react";
2
  import ReactDOM from "react-dom/client";
3
  import App from "./App.jsx";
4
+ import "./index.css";
5
 
6
  ReactDOM.createRoot(document.getElementById("root")).render(
7
  <React.StrictMode>
frontend/src/pages/Dashboard.jsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { createPageUrl } from '@/utils';
4
+ import { motion } from 'framer-motion';
5
+ import {
6
+ Calendar,
7
+ FileText,
8
+ Image,
9
+ TrendingUp,
10
+ Clock,
11
+ Zap,
12
+ ArrowRight,
13
+ Plus,
14
+ Sparkles,
15
+ BarChart3,
16
+ FolderOpen,
17
+ Layers
18
+ } from 'lucide-react';
19
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
20
+ import { Button } from '@/components/ui/button';
21
+ import { Badge } from '@/components/ui/badge';
22
+ import { Progress } from '@/components/ui/progress';
23
+
24
+ export default function Dashboard() {
25
+ const stats = [
26
+ { label: 'Scheduled Posts', value: '24', icon: Calendar, color: 'from-blue-500 to-indigo-600', change: '+12%' },
27
+ { label: 'Repository Assets', value: '156', icon: FolderOpen, color: 'from-emerald-500 to-teal-600', change: '+8%' },
28
+ { label: 'Posts This Month', value: '18', icon: TrendingUp, color: 'from-violet-500 to-purple-600', change: '+24%' },
29
+ { label: 'Engagement Rate', value: '4.8%', icon: BarChart3, color: 'from-amber-500 to-orange-600', change: '+2.1%' },
30
+ ];
31
+
32
+ const upcomingPosts = [
33
+ { id: 1, title: 'OCR Document Automation Benefits', type: 'Carousel', date: 'Today, 2:00 PM', product: 'OCR', status: 'ready' },
34
+ { id: 2, title: 'P2P Workflow Efficiency Guide', type: 'Cover Image + Content', date: 'Tomorrow, 10:00 AM', product: 'P2P', status: 'pending' },
35
+ { id: 3, title: 'O2C Webinar Announcement', type: 'Webinar Invite', date: 'Dec 28, 3:00 PM', product: 'O2C', status: 'draft' },
36
+ ];
37
+
38
+ const productStats = [
39
+ { name: 'Document Parsing (OCR)', posts: 8, assets: 45, progress: 75 },
40
+ { name: 'Purchase To Pay (P2P)', posts: 6, assets: 52, progress: 60 },
41
+ { name: 'Order to Cash (O2C)', posts: 10, assets: 59, progress: 85 },
42
+ ];
43
+
44
+ const container = {
45
+ hidden: { opacity: 0 },
46
+ show: {
47
+ opacity: 1,
48
+ transition: { staggerChildren: 0.1 }
49
+ }
50
+ };
51
+
52
+ const item = {
53
+ hidden: { opacity: 0, y: 20 },
54
+ show: { opacity: 1, y: 0 }
55
+ };
56
+
57
+ return (
58
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
59
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
60
+ {/* Header */}
61
+ <motion.div
62
+ initial={{ opacity: 0, y: -20 }}
63
+ animate={{ opacity: 1, y: 0 }}
64
+ className="mb-8"
65
+ >
66
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
67
+ <div>
68
+ <h1 className="text-3xl font-bold text-slate-900 tracking-tight">
69
+ Welcome back
70
+ </h1>
71
+ <p className="text-slate-500 mt-1">
72
+ Here's what's happening with your LinkedIn content
73
+ </p>
74
+ </div>
75
+ <div className="flex gap-3">
76
+ <Link to={createPageUrl('Scheduler')}>
77
+ <Button variant="outline" className="gap-2 border-slate-200 hover:bg-slate-50">
78
+ <Calendar className="w-4 h-4" />
79
+ Schedule
80
+ </Button>
81
+ </Link>
82
+ <Link to={createPageUrl('PostEditor')}>
83
+ <Button className="gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
84
+ <Plus className="w-4 h-4" />
85
+ Create Post
86
+ </Button>
87
+ </Link>
88
+ </div>
89
+ </div>
90
+ </motion.div>
91
+
92
+ {/* Stats Grid */}
93
+ <motion.div
94
+ variants={container}
95
+ initial="hidden"
96
+ animate="show"
97
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
98
+ >
99
+ {stats.map((stat, index) => (
100
+ <motion.div key={index} variants={item}>
101
+ <Card className="border-0 shadow-lg shadow-slate-200/50 hover:shadow-xl transition-all duration-300 overflow-hidden group">
102
+ <CardContent className="p-6 relative">
103
+ <div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-br ${stat.color} opacity-5 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500`} />
104
+ <div className="flex items-start justify-between relative">
105
+ <div>
106
+ <p className="text-sm font-medium text-slate-500">{stat.label}</p>
107
+ <p className="text-3xl font-bold text-slate-900 mt-2">{stat.value}</p>
108
+ <Badge variant="secondary" className="mt-2 bg-emerald-50 text-emerald-700 border-0">
109
+ {stat.change}
110
+ </Badge>
111
+ </div>
112
+ <div className={`p-3 rounded-xl bg-gradient-to-br ${stat.color} shadow-lg`}>
113
+ <stat.icon className="w-5 h-5 text-white" />
114
+ </div>
115
+ </div>
116
+ </CardContent>
117
+ </Card>
118
+ </motion.div>
119
+ ))}
120
+ </motion.div>
121
+
122
+ <div className="grid lg:grid-cols-3 gap-6">
123
+ {/* Upcoming Posts */}
124
+ <motion.div
125
+ initial={{ opacity: 0, x: -20 }}
126
+ animate={{ opacity: 1, x: 0 }}
127
+ transition={{ delay: 0.3 }}
128
+ className="lg:col-span-2"
129
+ >
130
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
131
+ <CardHeader className="pb-4">
132
+ <div className="flex items-center justify-between">
133
+ <CardTitle className="text-lg font-semibold flex items-center gap-2">
134
+ <Clock className="w-5 h-5 text-blue-600" />
135
+ Upcoming Posts
136
+ </CardTitle>
137
+ <Link to={createPageUrl('Scheduler')}>
138
+ <Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 gap-1">
139
+ View all <ArrowRight className="w-4 h-4" />
140
+ </Button>
141
+ </Link>
142
+ </div>
143
+ </CardHeader>
144
+ <CardContent className="space-y-3">
145
+ {upcomingPosts.map((post, index) => (
146
+ <motion.div
147
+ key={post.id}
148
+ initial={{ opacity: 0, y: 10 }}
149
+ animate={{ opacity: 1, y: 0 }}
150
+ transition={{ delay: 0.4 + index * 0.1 }}
151
+ className="group p-4 rounded-xl bg-slate-50/50 hover:bg-white hover:shadow-md border border-transparent hover:border-slate-100 transition-all duration-300 cursor-pointer"
152
+ >
153
+ <div className="flex items-center gap-4">
154
+ <div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
155
+ post.product === 'OCR' ? 'bg-blue-100 text-blue-600' :
156
+ post.product === 'P2P' ? 'bg-emerald-100 text-emerald-600' :
157
+ 'bg-violet-100 text-violet-600'
158
+ }`}>
159
+ {post.type === 'Carousel' ? <Layers className="w-5 h-5" /> :
160
+ post.type === 'Webinar Invite' ? <Sparkles className="w-5 h-5" /> :
161
+ <Image className="w-5 h-5" />}
162
+ </div>
163
+ <div className="flex-1 min-w-0">
164
+ <h3 className="font-medium text-slate-900 truncate">{post.title}</h3>
165
+ <div className="flex items-center gap-2 mt-1">
166
+ <span className="text-sm text-slate-500">{post.date}</span>
167
+ <span className="text-slate-300">•</span>
168
+ <Badge variant="outline" className="text-xs border-slate-200">
169
+ {post.type}
170
+ </Badge>
171
+ </div>
172
+ </div>
173
+ <Badge className={`capitalize ${
174
+ post.status === 'ready' ? 'bg-emerald-100 text-emerald-700 border-0' :
175
+ post.status === 'pending' ? 'bg-amber-100 text-amber-700 border-0' :
176
+ 'bg-slate-100 text-slate-600 border-0'
177
+ }`}>
178
+ {post.status}
179
+ </Badge>
180
+ </div>
181
+ </motion.div>
182
+ ))}
183
+ </CardContent>
184
+ </Card>
185
+ </motion.div>
186
+
187
+ {/* Product Distribution */}
188
+ <motion.div
189
+ initial={{ opacity: 0, x: 20 }}
190
+ animate={{ opacity: 1, x: 0 }}
191
+ transition={{ delay: 0.4 }}
192
+ >
193
+ <Card className="border-0 shadow-lg shadow-slate-200/50 h-full">
194
+ <CardHeader className="pb-4">
195
+ <CardTitle className="text-lg font-semibold flex items-center gap-2">
196
+ <Zap className="w-5 h-5 text-amber-500" />
197
+ Content by Product
198
+ </CardTitle>
199
+ </CardHeader>
200
+ <CardContent className="space-y-5">
201
+ {productStats.map((product, index) => (
202
+ <div key={index} className="space-y-2">
203
+ <div className="flex items-center justify-between">
204
+ <span className="text-sm font-medium text-slate-700">{product.name}</span>
205
+ <span className="text-xs text-slate-500">{product.posts} posts</span>
206
+ </div>
207
+ <Progress value={product.progress} className="h-2" />
208
+ <div className="flex items-center justify-between text-xs text-slate-500">
209
+ <span>{product.assets} assets</span>
210
+ <span>{product.progress}% coverage</span>
211
+ </div>
212
+ </div>
213
+ ))}
214
+ </CardContent>
215
+ </Card>
216
+ </motion.div>
217
+ </div>
218
+
219
+ {/* Quick Actions */}
220
+ <motion.div
221
+ initial={{ opacity: 0, y: 20 }}
222
+ animate={{ opacity: 1, y: 0 }}
223
+ transition={{ delay: 0.5 }}
224
+ className="mt-8"
225
+ >
226
+ <h2 className="text-lg font-semibold text-slate-900 mb-4">Quick Actions</h2>
227
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
228
+ {[
229
+ { label: 'Upload Assets', icon: FolderOpen, color: 'blue', href: 'Repository' },
230
+ { label: 'Schedule Campaign', icon: Calendar, color: 'violet', href: 'Scheduler' },
231
+ { label: 'Connect Canva', icon: Sparkles, color: 'pink', href: 'Integrations' },
232
+ { label: 'View Analytics', icon: BarChart3, color: 'emerald', href: 'Dashboard' },
233
+ ].map((action, index) => (
234
+ <Link key={index} to={createPageUrl(action.href)}>
235
+ <Card className="border-0 shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group overflow-hidden">
236
+ <CardContent className="p-5 flex items-center gap-4">
237
+ <div className={`p-3 rounded-xl bg-${action.color}-100 group-hover:scale-110 transition-transform`}>
238
+ <action.icon className={`w-5 h-5 text-${action.color}-600`} />
239
+ </div>
240
+ <span className="font-medium text-slate-700 group-hover:text-slate-900">{action.label}</span>
241
+ <ArrowRight className="w-4 h-4 text-slate-400 ml-auto group-hover:translate-x-1 transition-transform" />
242
+ </CardContent>
243
+ </Card>
244
+ </Link>
245
+ ))}
246
+ </div>
247
+ </motion.div>
248
+ </div>
249
+ </div>
250
+ );
251
+ }
252
+
frontend/src/pages/Integrations.jsx ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import {
4
+ Link2,
5
+ Check,
6
+ X,
7
+ ExternalLink,
8
+ Settings,
9
+ RefreshCw,
10
+ Shield,
11
+ Key,
12
+ AlertCircle,
13
+ CheckCircle,
14
+ Linkedin,
15
+ Palette,
16
+ Zap,
17
+ ArrowRight,
18
+ Info,
19
+ Save
20
+ } from 'lucide-react';
21
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
22
+ import { Button } from '@/components/ui/button';
23
+ import { Badge } from '@/components/ui/badge';
24
+ import { Switch } from '@/components/ui/switch';
25
+ import { Label } from '@/components/ui/label';
26
+ import { Input } from '@/components/ui/input';
27
+ import { Separator } from '@/components/ui/separator';
28
+ import {
29
+ Dialog,
30
+ DialogContent,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ DialogTrigger,
34
+ DialogDescription,
35
+ } from '@/components/ui/dialog';
36
+ import {
37
+ Alert,
38
+ AlertDescription,
39
+ AlertTitle,
40
+ } from '@/components/ui/alert';
41
+
42
+ const integrations = [
43
+ {
44
+ id: 'linkedin',
45
+ name: 'LinkedIn',
46
+ description: 'Connect your LinkedIn account to schedule and publish posts directly',
47
+ icon: Linkedin,
48
+ color: 'blue',
49
+ bgColor: 'bg-[#0A66C2]',
50
+ connected: true,
51
+ account: 'alex.business@company.com',
52
+ lastSync: '2 mins ago',
53
+ features: [
54
+ 'Schedule posts up to 60 days in advance',
55
+ 'Publish carousel, image, and text posts',
56
+ 'Track post performance analytics',
57
+ 'Manage multiple company pages'
58
+ ]
59
+ },
60
+ {
61
+ id: 'canva',
62
+ name: 'Canva',
63
+ description: 'Import designs directly from your Canva workspace for post visuals',
64
+ icon: Palette,
65
+ color: 'purple',
66
+ bgColor: 'bg-gradient-to-br from-[#00C4CC] to-[#7B2FF7]',
67
+ connected: false,
68
+ account: null,
69
+ lastSync: null,
70
+ features: [
71
+ 'Browse and import Canva designs',
72
+ 'Access brand kit assets',
73
+ 'Import templates for carousels',
74
+ 'Sync design updates automatically'
75
+ ]
76
+ },
77
+ ];
78
+
79
+ export default function Integrations() {
80
+ const [linkedInDialogOpen, setLinkedInDialogOpen] = useState(false);
81
+ const [canvaDialogOpen, setCanvaDialogOpen] = useState(false);
82
+ const [autoPost, setAutoPost] = useState(true);
83
+ const [notifications, setNotifications] = useState(true);
84
+
85
+ return (
86
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
87
+ <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
88
+ {/* Header */}
89
+ <motion.div
90
+ initial={{ opacity: 0, y: -20 }}
91
+ animate={{ opacity: 1, y: 0 }}
92
+ className="mb-8"
93
+ >
94
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
95
+ <div>
96
+ <h1 className="text-3xl font-bold text-slate-900 tracking-tight">
97
+ Integrations
98
+ </h1>
99
+ <p className="text-slate-500 mt-1">
100
+ Connect your tools to streamline your content workflow
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </motion.div>
105
+
106
+ {/* Integration Status Banner */}
107
+ <motion.div
108
+ initial={{ opacity: 0, y: 10 }}
109
+ animate={{ opacity: 1, y: 0 }}
110
+ className="mb-8"
111
+ >
112
+ <Alert className="border-blue-200 bg-blue-50">
113
+ <Info className="h-4 w-4 text-blue-600" />
114
+ <AlertTitle className="text-blue-800">Quick Setup</AlertTitle>
115
+ <AlertDescription className="text-blue-700">
116
+ Connect both LinkedIn and Canva to unlock the full potential of automated content scheduling. Your designs will flow seamlessly to your LinkedIn audience.
117
+ </AlertDescription>
118
+ </Alert>
119
+ </motion.div>
120
+
121
+ {/* Integration Cards */}
122
+ <div className="space-y-6">
123
+ {integrations.map((integration, index) => (
124
+ <motion.div
125
+ key={integration.id}
126
+ initial={{ opacity: 0, y: 20 }}
127
+ animate={{ opacity: 1, y: 0 }}
128
+ transition={{ delay: index * 0.1 }}
129
+ >
130
+ <Card className="border-0 shadow-lg shadow-slate-200/50 overflow-hidden">
131
+ <CardContent className="p-0">
132
+ <div className="flex flex-col lg:flex-row">
133
+ {/* Left Section - Integration Info */}
134
+ <div className="flex-1 p-6">
135
+ <div className="flex items-start gap-4">
136
+ <div className={`w-14 h-14 rounded-xl ${integration.bgColor} flex items-center justify-center shadow-lg`}>
137
+ <integration.icon className="w-7 h-7 text-white" />
138
+ </div>
139
+ <div className="flex-1">
140
+ <div className="flex items-center gap-3">
141
+ <h3 className="text-xl font-semibold text-slate-900">
142
+ {integration.name}
143
+ </h3>
144
+ {integration.connected ? (
145
+ <Badge className="bg-emerald-100 text-emerald-700 border-0 gap-1">
146
+ <CheckCircle className="w-3 h-3" />
147
+ Connected
148
+ </Badge>
149
+ ) : (
150
+ <Badge variant="outline" className="text-slate-500 border-slate-300">
151
+ Not Connected
152
+ </Badge>
153
+ )}
154
+ </div>
155
+ <p className="text-slate-500 mt-1">
156
+ {integration.description}
157
+ </p>
158
+
159
+ {integration.connected && (
160
+ <div className="flex items-center gap-4 mt-3">
161
+ <div className="flex items-center gap-2 text-sm">
162
+ <Key className="w-4 h-4 text-slate-400" />
163
+ <span className="text-slate-600">{integration.account}</span>
164
+ </div>
165
+ <div className="flex items-center gap-2 text-sm">
166
+ <RefreshCw className="w-4 h-4 text-slate-400" />
167
+ <span className="text-slate-500">Synced {integration.lastSync}</span>
168
+ </div>
169
+ </div>
170
+ )}
171
+ </div>
172
+ </div>
173
+
174
+ {/* Features */}
175
+ <div className="mt-6">
176
+ <p className="text-sm font-medium text-slate-700 mb-3">Features</p>
177
+ <div className="grid sm:grid-cols-2 gap-2">
178
+ {integration.features.map((feature, idx) => (
179
+ <div key={idx} className="flex items-center gap-2 text-sm text-slate-600">
180
+ <Check className="w-4 h-4 text-emerald-500 shrink-0" />
181
+ <span>{feature}</span>
182
+ </div>
183
+ ))}
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ {/* Right Section - Actions */}
189
+ <div className="lg:w-64 p-6 bg-slate-50 border-t lg:border-t-0 lg:border-l border-slate-100 flex flex-col justify-center">
190
+ {integration.connected ? (
191
+ <div className="space-y-4">
192
+ <Dialog>
193
+ <DialogTrigger asChild>
194
+ <Button variant="outline" className="w-full gap-2">
195
+ <Settings className="w-4 h-4" />
196
+ Settings
197
+ </Button>
198
+ </DialogTrigger>
199
+ <DialogContent>
200
+ <DialogHeader>
201
+ <DialogTitle className="flex items-center gap-3">
202
+ <div className={`w-10 h-10 rounded-lg ${integration.bgColor} flex items-center justify-center`}>
203
+ <integration.icon className="w-5 h-5 text-white" />
204
+ </div>
205
+ {integration.name} Settings
206
+ </DialogTitle>
207
+ <DialogDescription>
208
+ Configure your {integration.name} integration preferences
209
+ </DialogDescription>
210
+ </DialogHeader>
211
+ <div className="space-y-6 pt-4">
212
+ <div className="flex items-center justify-between">
213
+ <div>
214
+ <Label className="text-sm font-medium">Auto-post scheduled content</Label>
215
+ <p className="text-xs text-slate-500 mt-0.5">
216
+ Automatically publish posts at scheduled times
217
+ </p>
218
+ </div>
219
+ <Switch checked={autoPost} onCheckedChange={setAutoPost} />
220
+ </div>
221
+ <Separator />
222
+ <div className="flex items-center justify-between">
223
+ <div>
224
+ <Label className="text-sm font-medium">Post notifications</Label>
225
+ <p className="text-xs text-slate-500 mt-0.5">
226
+ Get notified when posts are published
227
+ </p>
228
+ </div>
229
+ <Switch checked={notifications} onCheckedChange={setNotifications} />
230
+ </div>
231
+ <Separator />
232
+ <div>
233
+ <Label className="text-sm font-medium">Connected Account</Label>
234
+ <div className="flex items-center gap-3 mt-2 p-3 rounded-lg bg-slate-50">
235
+ <div className={`w-8 h-8 rounded-lg ${integration.bgColor} flex items-center justify-center`}>
236
+ <integration.icon className="w-4 h-4 text-white" />
237
+ </div>
238
+ <div className="flex-1">
239
+ <p className="text-sm font-medium text-slate-700">{integration.account}</p>
240
+ <p className="text-xs text-slate-500">Connected</p>
241
+ </div>
242
+ <Button variant="ghost" size="sm" className="text-red-600 hover:text-red-700 hover:bg-red-50">
243
+ Disconnect
244
+ </Button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </DialogContent>
249
+ </Dialog>
250
+ <Button variant="ghost" className="w-full gap-2 text-red-600 hover:text-red-700 hover:bg-red-50">
251
+ <X className="w-4 h-4" />
252
+ Disconnect
253
+ </Button>
254
+ </div>
255
+ ) : (
256
+ <div className="space-y-4">
257
+ <Dialog>
258
+ <DialogTrigger asChild>
259
+ <Button className={`w-full gap-2 ${
260
+ integration.id === 'linkedin'
261
+ ? 'bg-[#0A66C2] hover:bg-[#004182]'
262
+ : 'bg-gradient-to-r from-[#00C4CC] to-[#7B2FF7] hover:opacity-90'
263
+ }`}>
264
+ <Link2 className="w-4 h-4" />
265
+ Connect {integration.name}
266
+ </Button>
267
+ </DialogTrigger>
268
+ <DialogContent>
269
+ <DialogHeader>
270
+ <DialogTitle className="flex items-center gap-3">
271
+ <div className={`w-10 h-10 rounded-lg ${integration.bgColor} flex items-center justify-center`}>
272
+ <integration.icon className="w-5 h-5 text-white" />
273
+ </div>
274
+ Connect to {integration.name}
275
+ </DialogTitle>
276
+ <DialogDescription>
277
+ Authorize access to your {integration.name} account
278
+ </DialogDescription>
279
+ </DialogHeader>
280
+ <div className="space-y-6 pt-4">
281
+ <div className="p-4 rounded-xl bg-slate-50 border border-slate-100">
282
+ <div className="flex items-start gap-3">
283
+ <Shield className="w-5 h-5 text-blue-600 mt-0.5" />
284
+ <div>
285
+ <p className="text-sm font-medium text-slate-700">Secure Connection</p>
286
+ <p className="text-xs text-slate-500 mt-1">
287
+ Your credentials are encrypted and never stored. We only request necessary permissions.
288
+ </p>
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ <div>
294
+ <Label className="text-sm font-medium">Permissions Requested</Label>
295
+ <div className="mt-2 space-y-2">
296
+ {integration.id === 'linkedin' ? (
297
+ <>
298
+ <div className="flex items-center gap-2 text-sm text-slate-600">
299
+ <Check className="w-4 h-4 text-emerald-500" />
300
+ Read and write posts on your behalf
301
+ </div>
302
+ <div className="flex items-center gap-2 text-sm text-slate-600">
303
+ <Check className="w-4 h-4 text-emerald-500" />
304
+ Schedule posts for future publishing
305
+ </div>
306
+ <div className="flex items-center gap-2 text-sm text-slate-600">
307
+ <Check className="w-4 h-4 text-emerald-500" />
308
+ Access post analytics and insights
309
+ </div>
310
+ </>
311
+ ) : (
312
+ <>
313
+ <div className="flex items-center gap-2 text-sm text-slate-600">
314
+ <Check className="w-4 h-4 text-emerald-500" />
315
+ View your Canva designs
316
+ </div>
317
+ <div className="flex items-center gap-2 text-sm text-slate-600">
318
+ <Check className="w-4 h-4 text-emerald-500" />
319
+ Download designs as images
320
+ </div>
321
+ <div className="flex items-center gap-2 text-sm text-slate-600">
322
+ <Check className="w-4 h-4 text-emerald-500" />
323
+ Access brand kit assets
324
+ </div>
325
+ </>
326
+ )}
327
+ </div>
328
+ </div>
329
+
330
+ <Button className={`w-full gap-2 ${
331
+ integration.id === 'linkedin'
332
+ ? 'bg-[#0A66C2] hover:bg-[#004182]'
333
+ : 'bg-gradient-to-r from-[#00C4CC] to-[#7B2FF7] hover:opacity-90'
334
+ }`}>
335
+ Continue with {integration.name}
336
+ <ExternalLink className="w-4 h-4" />
337
+ </Button>
338
+ </div>
339
+ </DialogContent>
340
+ </Dialog>
341
+ <p className="text-xs text-center text-slate-500">
342
+ You'll be redirected to {integration.name} to authorize
343
+ </p>
344
+ </div>
345
+ )}
346
+ </div>
347
+ </div>
348
+ </CardContent>
349
+ </Card>
350
+ </motion.div>
351
+ ))}
352
+ </div>
353
+
354
+ {/* API Configuration */}
355
+ <motion.div
356
+ initial={{ opacity: 0, y: 20 }}
357
+ animate={{ opacity: 1, y: 0 }}
358
+ transition={{ delay: 0.3 }}
359
+ className="mt-8"
360
+ >
361
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
362
+ <CardHeader>
363
+ <CardTitle className="text-lg font-semibold flex items-center gap-2">
364
+ <Key className="w-5 h-5 text-amber-500" />
365
+ API Configuration
366
+ </CardTitle>
367
+ <CardDescription>
368
+ For advanced users who want to configure custom API settings
369
+ </CardDescription>
370
+ </CardHeader>
371
+ <CardContent className="space-y-4">
372
+ <div className="grid md:grid-cols-2 gap-4">
373
+ <div>
374
+ <Label className="text-sm text-slate-600">LinkedIn Client ID</Label>
375
+ <Input
376
+ className="mt-1.5"
377
+ placeholder="Enter your LinkedIn Client ID"
378
+ type="password"
379
+ defaultValue="••••••••••••••••"
380
+ />
381
+ </div>
382
+ <div>
383
+ <Label className="text-sm text-slate-600">LinkedIn Client Secret</Label>
384
+ <Input
385
+ className="mt-1.5"
386
+ placeholder="Enter your LinkedIn Client Secret"
387
+ type="password"
388
+ defaultValue="••••••••••••••••"
389
+ />
390
+ </div>
391
+ <div>
392
+ <Label className="text-sm text-slate-600">Canva API Key</Label>
393
+ <Input
394
+ className="mt-1.5"
395
+ placeholder="Enter your Canva API Key"
396
+ type="password"
397
+ />
398
+ </div>
399
+ <div>
400
+ <Label className="text-sm text-slate-600">Webhook URL</Label>
401
+ <Input
402
+ className="mt-1.5"
403
+ placeholder="https://your-webhook-url.com"
404
+ />
405
+ </div>
406
+ </div>
407
+ <div className="flex justify-end">
408
+ <Button variant="outline" className="gap-2">
409
+ <Save className="w-4 h-4" />
410
+ Save Configuration
411
+ </Button>
412
+ </div>
413
+ </CardContent>
414
+ </Card>
415
+ </motion.div>
416
+ </div>
417
+ </div>
418
+ );
419
+ }
420
+
frontend/src/pages/PostEditor.jsx ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import {
4
+ Save,
5
+ Send,
6
+ Eye,
7
+ Calendar,
8
+ Clock,
9
+ Image,
10
+ Layers,
11
+ FileText,
12
+ Video,
13
+ Sparkles,
14
+ ChevronDown,
15
+ Plus,
16
+ X,
17
+ GripVertical,
18
+ RefreshCw,
19
+ ThumbsUp,
20
+ MessageCircle,
21
+ Share2,
22
+ MoreHorizontal,
23
+ Globe,
24
+ Link2,
25
+ Hash,
26
+ AtSign,
27
+ Bold,
28
+ Italic,
29
+ List,
30
+ Smile,
31
+ ImagePlus,
32
+ Trash2,
33
+ ArrowLeft,
34
+ Linkedin
35
+ } from 'lucide-react';
36
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
37
+ import { Button } from '@/components/ui/button';
38
+ import { Input } from '@/components/ui/input';
39
+ import { Textarea } from '@/components/ui/textarea';
40
+ import { Badge } from '@/components/ui/badge';
41
+ import { Label } from '@/components/ui/label';
42
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
43
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
44
+ import { Separator } from '@/components/ui/separator';
45
+ import {
46
+ Select,
47
+ SelectContent,
48
+ SelectItem,
49
+ SelectTrigger,
50
+ SelectValue,
51
+ } from '@/components/ui/select';
52
+ import {
53
+ Popover,
54
+ PopoverContent,
55
+ PopoverTrigger,
56
+ } from '@/components/ui/popover';
57
+ import { Calendar as CalendarComponent } from '@/components/ui/calendar';
58
+ import { format } from 'date-fns';
59
+ import { Link } from 'react-router-dom';
60
+ import { createPageUrl } from '@/utils';
61
+
62
+ const postTypes = [
63
+ { id: 'carousel', name: 'Carousel', icon: Layers },
64
+ { id: 'cover_content', name: 'Cover Image + Content', icon: Image },
65
+ { id: 'content_only', name: 'Content Only', icon: FileText },
66
+ { id: 'webinar', name: 'Webinar Invite', icon: Video },
67
+ ];
68
+
69
+ const products = [
70
+ { id: 'ocr', name: 'Document Parsing (OCR)', shortName: 'OCR' },
71
+ { id: 'p2p', name: 'Purchase To Pay', shortName: 'P2P' },
72
+ { id: 'o2c', name: 'Order to Cash', shortName: 'O2C' },
73
+ ];
74
+
75
+ export default function PostEditor() {
76
+ const [postType, setPostType] = useState('carousel');
77
+ const [product, setProduct] = useState('ocr');
78
+ const [content, setContent] = useState(`🚀 Transform Your Document Processing with AI-Powered OCR
79
+
80
+ Are you still manually processing invoices and documents?
81
+
82
+ Here's how our Intelligent Document Parsing solution can revolutionize your workflow:
83
+
84
+ ✅ 99.5% accuracy in data extraction
85
+ ✅ Process 1000+ documents per hour
86
+ ✅ Seamless integration with existing systems
87
+ ✅ Reduce manual errors by 95%
88
+
89
+ The future of document automation is here. Ready to transform your business?
90
+
91
+ #DocumentAutomation #OCR #AITechnology #DigitalTransformation #BusinessEfficiency`);
92
+ const [scheduledDate, setScheduledDate] = useState(new Date());
93
+ const [scheduledTime, setScheduledTime] = useState('10:00');
94
+ const [carouselSlides, setCarouselSlides] = useState([
95
+ { id: 1, title: 'Slide 1', hasImage: true },
96
+ { id: 2, title: 'Slide 2', hasImage: true },
97
+ { id: 3, title: 'Slide 3', hasImage: true },
98
+ ]);
99
+ const [selectedAssets, setSelectedAssets] = useState([
100
+ { id: 1, name: 'OCR_Demo.png', type: 'image' },
101
+ { id: 2, name: 'Workflow_Diagram.png', type: 'image' },
102
+ ]);
103
+
104
+ const addSlide = () => {
105
+ setCarouselSlides([...carouselSlides, { id: Date.now(), title: `Slide ${carouselSlides.length + 1}`, hasImage: false }]);
106
+ };
107
+
108
+ const removeSlide = (id) => {
109
+ setCarouselSlides(carouselSlides.filter(slide => slide.id !== id));
110
+ };
111
+
112
+ return (
113
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
114
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
115
+ {/* Header */}
116
+ <motion.div
117
+ initial={{ opacity: 0, y: -20 }}
118
+ animate={{ opacity: 1, y: 0 }}
119
+ className="mb-8"
120
+ >
121
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
122
+ <div className="flex items-center gap-4">
123
+ <Link to={createPageUrl('Scheduler')}>
124
+ <Button variant="ghost" size="icon" className="h-10 w-10">
125
+ <ArrowLeft className="w-5 h-5" />
126
+ </Button>
127
+ </Link>
128
+ <div>
129
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight">
130
+ Create Post
131
+ </h1>
132
+ <p className="text-slate-500 text-sm mt-0.5">
133
+ Compose and schedule your LinkedIn content
134
+ </p>
135
+ </div>
136
+ </div>
137
+ <div className="flex gap-3">
138
+ <Button variant="outline" className="gap-2 border-slate-200">
139
+ <Save className="w-4 h-4" />
140
+ Save Draft
141
+ </Button>
142
+ <Button variant="outline" className="gap-2 border-slate-200">
143
+ <Eye className="w-4 h-4" />
144
+ Preview
145
+ </Button>
146
+ <Button className="gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
147
+ <Send className="w-4 h-4" />
148
+ Schedule Post
149
+ </Button>
150
+ </div>
151
+ </div>
152
+ </motion.div>
153
+
154
+ <div className="grid lg:grid-cols-5 gap-6">
155
+ {/* Editor Panel */}
156
+ <motion.div
157
+ initial={{ opacity: 0, x: -20 }}
158
+ animate={{ opacity: 1, x: 0 }}
159
+ className="lg:col-span-3 space-y-6"
160
+ >
161
+ {/* Post Type Selection */}
162
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
163
+ <CardHeader className="pb-4">
164
+ <CardTitle className="text-base font-semibold">Post Type</CardTitle>
165
+ </CardHeader>
166
+ <CardContent>
167
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
168
+ {postTypes.map(type => (
169
+ <button
170
+ key={type.id}
171
+ onClick={() => setPostType(type.id)}
172
+ className={`p-4 rounded-xl border-2 transition-all ${
173
+ postType === type.id
174
+ ? 'border-blue-500 bg-blue-50'
175
+ : 'border-slate-200 hover:border-slate-300 bg-white'
176
+ }`}
177
+ >
178
+ <type.icon className={`w-6 h-6 mx-auto mb-2 ${
179
+ postType === type.id ? 'text-blue-600' : 'text-slate-400'
180
+ }`} />
181
+ <p className={`text-sm font-medium ${
182
+ postType === type.id ? 'text-blue-700' : 'text-slate-600'
183
+ }`}>
184
+ {type.name}
185
+ </p>
186
+ </button>
187
+ ))}
188
+ </div>
189
+ </CardContent>
190
+ </Card>
191
+
192
+ {/* Content Editor */}
193
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
194
+ <CardHeader className="pb-4">
195
+ <div className="flex items-center justify-between">
196
+ <CardTitle className="text-base font-semibold">Content</CardTitle>
197
+ <Button variant="outline" size="sm" className="gap-2 text-violet-600 border-violet-200 hover:bg-violet-50">
198
+ <Sparkles className="w-4 h-4" />
199
+ AI Generate
200
+ </Button>
201
+ </div>
202
+ </CardHeader>
203
+ <CardContent className="space-y-4">
204
+ {/* Formatting Toolbar */}
205
+ <div className="flex items-center gap-1 p-2 bg-slate-50 rounded-lg">
206
+ <Button variant="ghost" size="icon" className="h-8 w-8">
207
+ <Bold className="w-4 h-4" />
208
+ </Button>
209
+ <Button variant="ghost" size="icon" className="h-8 w-8">
210
+ <Italic className="w-4 h-4" />
211
+ </Button>
212
+ <Button variant="ghost" size="icon" className="h-8 w-8">
213
+ <List className="w-4 h-4" />
214
+ </Button>
215
+ <Separator orientation="vertical" className="h-6 mx-1" />
216
+ <Button variant="ghost" size="icon" className="h-8 w-8">
217
+ <Hash className="w-4 h-4" />
218
+ </Button>
219
+ <Button variant="ghost" size="icon" className="h-8 w-8">
220
+ <AtSign className="w-4 h-4" />
221
+ </Button>
222
+ <Button variant="ghost" size="icon" className="h-8 w-8">
223
+ <Link2 className="w-4 h-4" />
224
+ </Button>
225
+ <Button variant="ghost" size="icon" className="h-8 w-8">
226
+ <Smile className="w-4 h-4" />
227
+ </Button>
228
+ </div>
229
+
230
+ <Textarea
231
+ value={content}
232
+ onChange={(e) => setContent(e.target.value)}
233
+ className="min-h-[250px] text-sm leading-relaxed border-slate-200 focus:border-blue-300"
234
+ placeholder="Write your post content here..."
235
+ />
236
+
237
+ <div className="flex items-center justify-between text-sm text-slate-500">
238
+ <span>{content.length} / 3000 characters</span>
239
+ <Button variant="ghost" size="sm" className="gap-1 text-blue-600">
240
+ <RefreshCw className="w-3 h-3" />
241
+ Regenerate
242
+ </Button>
243
+ </div>
244
+ </CardContent>
245
+ </Card>
246
+
247
+ {/* Carousel Slides (if carousel type) */}
248
+ {postType === 'carousel' && (
249
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
250
+ <CardHeader className="pb-4">
251
+ <div className="flex items-center justify-between">
252
+ <CardTitle className="text-base font-semibold">Carousel Slides</CardTitle>
253
+ <Button onClick={addSlide} variant="outline" size="sm" className="gap-1">
254
+ <Plus className="w-4 h-4" />
255
+ Add Slide
256
+ </Button>
257
+ </div>
258
+ </CardHeader>
259
+ <CardContent>
260
+ <div className="space-y-3">
261
+ {carouselSlides.map((slide, index) => (
262
+ <motion.div
263
+ key={slide.id}
264
+ initial={{ opacity: 0, y: 10 }}
265
+ animate={{ opacity: 1, y: 0 }}
266
+ className="flex items-center gap-3 p-3 rounded-xl border border-slate-200 bg-white hover:border-slate-300 transition-colors"
267
+ >
268
+ <GripVertical className="w-4 h-4 text-slate-400 cursor-grab" />
269
+ <div className="w-16 h-16 rounded-lg bg-gradient-to-br from-slate-100 to-slate-50 flex items-center justify-center border border-slate-200">
270
+ {slide.hasImage ? (
271
+ <Image className="w-6 h-6 text-slate-400" />
272
+ ) : (
273
+ <ImagePlus className="w-6 h-6 text-slate-300" />
274
+ )}
275
+ </div>
276
+ <div className="flex-1">
277
+ <Input
278
+ value={slide.title}
279
+ className="h-8 text-sm border-0 bg-transparent p-0 font-medium"
280
+ placeholder="Slide title"
281
+ />
282
+ <p className="text-xs text-slate-500 mt-0.5">
283
+ {slide.hasImage ? 'Image attached' : 'No image'}
284
+ </p>
285
+ </div>
286
+ <Button variant="ghost" size="sm" className="gap-1 text-blue-600">
287
+ <ImagePlus className="w-4 h-4" />
288
+ {slide.hasImage ? 'Change' : 'Add'} Image
289
+ </Button>
290
+ <Button
291
+ variant="ghost"
292
+ size="icon"
293
+ onClick={() => removeSlide(slide.id)}
294
+ className="h-8 w-8 text-slate-400 hover:text-red-500"
295
+ >
296
+ <Trash2 className="w-4 h-4" />
297
+ </Button>
298
+ </motion.div>
299
+ ))}
300
+ </div>
301
+ </CardContent>
302
+ </Card>
303
+ )}
304
+
305
+ {/* Media Attachments */}
306
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
307
+ <CardHeader className="pb-4">
308
+ <div className="flex items-center justify-between">
309
+ <CardTitle className="text-base font-semibold">Media from Repository</CardTitle>
310
+ <Link to={createPageUrl('Repository')}>
311
+ <Button variant="outline" size="sm" className="gap-1">
312
+ <Plus className="w-4 h-4" />
313
+ Browse Assets
314
+ </Button>
315
+ </Link>
316
+ </div>
317
+ </CardHeader>
318
+ <CardContent>
319
+ <div className="flex flex-wrap gap-3">
320
+ {selectedAssets.map(asset => (
321
+ <div
322
+ key={asset.id}
323
+ className="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-50 border border-slate-200"
324
+ >
325
+ <Image className="w-4 h-4 text-blue-500" />
326
+ <span className="text-sm text-slate-700">{asset.name}</span>
327
+ <Button variant="ghost" size="icon" className="h-5 w-5 text-slate-400 hover:text-red-500">
328
+ <X className="w-3 h-3" />
329
+ </Button>
330
+ </div>
331
+ ))}
332
+ <button className="flex items-center gap-2 px-3 py-2 rounded-lg border-2 border-dashed border-slate-200 text-slate-500 hover:border-blue-300 hover:text-blue-600 transition-colors">
333
+ <Plus className="w-4 h-4" />
334
+ <span className="text-sm">Add from Canva</span>
335
+ </button>
336
+ </div>
337
+ </CardContent>
338
+ </Card>
339
+ </motion.div>
340
+
341
+ {/* Preview & Settings Panel */}
342
+ <motion.div
343
+ initial={{ opacity: 0, x: 20 }}
344
+ animate={{ opacity: 1, x: 0 }}
345
+ className="lg:col-span-2 space-y-6"
346
+ >
347
+ {/* Schedule Settings */}
348
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
349
+ <CardHeader className="pb-4">
350
+ <CardTitle className="text-base font-semibold flex items-center gap-2">
351
+ <Calendar className="w-4 h-4 text-blue-600" />
352
+ Schedule
353
+ </CardTitle>
354
+ </CardHeader>
355
+ <CardContent className="space-y-4">
356
+ <div>
357
+ <Label className="text-sm text-slate-600">Product Category</Label>
358
+ <Select value={product} onValueChange={setProduct}>
359
+ <SelectTrigger className="mt-1.5">
360
+ <SelectValue />
361
+ </SelectTrigger>
362
+ <SelectContent>
363
+ {products.map(p => (
364
+ <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
365
+ ))}
366
+ </SelectContent>
367
+ </Select>
368
+ </div>
369
+
370
+ <div className="grid grid-cols-2 gap-3">
371
+ <div>
372
+ <Label className="text-sm text-slate-600">Date</Label>
373
+ <Popover>
374
+ <PopoverTrigger asChild>
375
+ <Button variant="outline" className="w-full justify-start mt-1.5 text-left font-normal">
376
+ <Calendar className="w-4 h-4 mr-2" />
377
+ {format(scheduledDate, 'MMM d, yyyy')}
378
+ </Button>
379
+ </PopoverTrigger>
380
+ <PopoverContent className="w-auto p-0" align="start">
381
+ <CalendarComponent
382
+ mode="single"
383
+ selected={scheduledDate}
384
+ onSelect={setScheduledDate}
385
+ />
386
+ </PopoverContent>
387
+ </Popover>
388
+ </div>
389
+ <div>
390
+ <Label className="text-sm text-slate-600">Time</Label>
391
+ <Select value={scheduledTime} onValueChange={setScheduledTime}>
392
+ <SelectTrigger className="mt-1.5">
393
+ <Clock className="w-4 h-4 mr-2" />
394
+ <SelectValue />
395
+ </SelectTrigger>
396
+ <SelectContent>
397
+ {['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00'].map(time => (
398
+ <SelectItem key={time} value={time}>{time}</SelectItem>
399
+ ))}
400
+ </SelectContent>
401
+ </Select>
402
+ </div>
403
+ </div>
404
+ </CardContent>
405
+ </Card>
406
+
407
+ {/* LinkedIn Preview */}
408
+ <Card className="border-0 shadow-lg shadow-slate-200/50 overflow-hidden">
409
+ <CardHeader className="pb-3 bg-gradient-to-r from-[#0A66C2] to-[#004182]">
410
+ <CardTitle className="text-base font-semibold text-white flex items-center gap-2">
411
+ <Linkedin className="w-4 h-4" />
412
+ LinkedIn Preview
413
+ </CardTitle>
414
+ </CardHeader>
415
+ <CardContent className="p-0">
416
+ <div className="p-4 bg-white">
417
+ {/* Post Header */}
418
+ <div className="flex items-start gap-3">
419
+ <Avatar className="h-12 w-12">
420
+ <AvatarImage src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop" />
421
+ <AvatarFallback>AB</AvatarFallback>
422
+ </Avatar>
423
+ <div className="flex-1">
424
+ <p className="font-semibold text-sm text-slate-900">Alex Business</p>
425
+ <p className="text-xs text-slate-500">Marketing Director at TechCorp</p>
426
+ <p className="text-xs text-slate-400 flex items-center gap-1 mt-0.5">
427
+ {format(scheduledDate, 'MMM d')} • <Globe className="w-3 h-3" />
428
+ </p>
429
+ </div>
430
+ <Button variant="ghost" size="icon" className="h-8 w-8">
431
+ <MoreHorizontal className="w-4 h-4" />
432
+ </Button>
433
+ </div>
434
+
435
+ {/* Post Content */}
436
+ <div className="mt-3 text-sm text-slate-800 whitespace-pre-line leading-relaxed">
437
+ {content.length > 300 ? content.slice(0, 300) + '...' : content}
438
+ {content.length > 300 && (
439
+ <button className="text-blue-600 hover:underline ml-1">see more</button>
440
+ )}
441
+ </div>
442
+
443
+ {/* Post Image Preview */}
444
+ {postType !== 'content_only' && (
445
+ <div className="mt-3 -mx-4">
446
+ <div className="aspect-video bg-gradient-to-br from-blue-100 to-indigo-100 flex items-center justify-center">
447
+ {postType === 'carousel' ? (
448
+ <div className="text-center">
449
+ <Layers className="w-12 h-12 text-blue-400 mx-auto mb-2" />
450
+ <p className="text-sm text-blue-600">{carouselSlides.length} slides</p>
451
+ </div>
452
+ ) : (
453
+ <Image className="w-12 h-12 text-blue-400" />
454
+ )}
455
+ </div>
456
+ </div>
457
+ )}
458
+
459
+ {/* Engagement Stats */}
460
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100">
461
+ <div className="flex items-center gap-1 text-xs text-slate-500">
462
+ <div className="flex -space-x-1">
463
+ <div className="w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center">
464
+ <ThumbsUp className="w-2 h-2 text-white" />
465
+ </div>
466
+ <div className="w-4 h-4 rounded-full bg-red-500 flex items-center justify-center">
467
+ <span className="text-[8px]">❤️</span>
468
+ </div>
469
+ </div>
470
+ <span>Preview mode</span>
471
+ </div>
472
+ <span className="text-xs text-slate-500">0 comments</span>
473
+ </div>
474
+
475
+ {/* Action Buttons */}
476
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100">
477
+ {[
478
+ { icon: ThumbsUp, label: 'Like' },
479
+ { icon: MessageCircle, label: 'Comment' },
480
+ { icon: Share2, label: 'Share' },
481
+ { icon: Send, label: 'Send' },
482
+ ].map((action, idx) => (
483
+ <button
484
+ key={idx}
485
+ className="flex items-center gap-1.5 px-3 py-2 text-slate-600 hover:bg-slate-50 rounded-lg transition-colors"
486
+ >
487
+ <action.icon className="w-4 h-4" />
488
+ <span className="text-xs font-medium">{action.label}</span>
489
+ </button>
490
+ ))}
491
+ </div>
492
+ </div>
493
+ </CardContent>
494
+ </Card>
495
+
496
+ {/* Action Buttons */}
497
+ <div className="space-y-3">
498
+ <Button className="w-full gap-2 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
499
+ <Send className="w-5 h-5" />
500
+ Schedule for {format(scheduledDate, 'MMM d')} at {scheduledTime}
501
+ </Button>
502
+ <Button variant="outline" className="w-full gap-2 h-12 border-slate-200">
503
+ <Eye className="w-5 h-5" />
504
+ Post Immediately
505
+ </Button>
506
+ </div>
507
+ </motion.div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ );
512
+ }
513
+
frontend/src/pages/Repository.jsx ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Upload,
5
+ FolderOpen,
6
+ Image,
7
+ FileText,
8
+ Video,
9
+ Search,
10
+ Filter,
11
+ Grid3X3,
12
+ List,
13
+ MoreVertical,
14
+ Download,
15
+ Trash2,
16
+ Eye,
17
+ ChevronDown,
18
+ ChevronRight,
19
+ Plus,
20
+ X,
21
+ Check,
22
+ File
23
+ } from 'lucide-react';
24
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
+ import { Button } from '@/components/ui/button';
26
+ import { Input } from '@/components/ui/input';
27
+ import { Badge } from '@/components/ui/badge';
28
+ import { Label } from '@/components/ui/label';
29
+ import {
30
+ Select,
31
+ SelectContent,
32
+ SelectItem,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ } from '@/components/ui/select';
36
+ import {
37
+ DropdownMenu,
38
+ DropdownMenuContent,
39
+ DropdownMenuItem,
40
+ DropdownMenuTrigger,
41
+ } from '@/components/ui/dropdown-menu';
42
+ import {
43
+ Dialog,
44
+ DialogContent,
45
+ DialogHeader,
46
+ DialogTitle,
47
+ DialogTrigger,
48
+ } from '@/components/ui/dialog';
49
+ import { Checkbox } from '@/components/ui/checkbox';
50
+
51
+ const products = [
52
+ {
53
+ id: 'ocr',
54
+ name: 'Intelligent Document Parsing (OCR)',
55
+ shortName: 'OCR',
56
+ color: 'blue',
57
+ subCategories: []
58
+ },
59
+ {
60
+ id: 'p2p',
61
+ name: 'Purchase To Pay (P2P)',
62
+ shortName: 'P2P',
63
+ color: 'emerald',
64
+ subCategories: [
65
+ 'Budget Approval Workflow',
66
+ 'Purchase Request Workflow',
67
+ 'Accounts Payable Workflow'
68
+ ]
69
+ },
70
+ {
71
+ id: 'o2c',
72
+ name: 'Order to Cash (O2C)',
73
+ shortName: 'O2C',
74
+ color: 'violet',
75
+ subCategories: [
76
+ 'Quotation Workflow',
77
+ 'Sales Order Workflow',
78
+ 'PickSlip Delivery Workflow',
79
+ 'Accounts Receivable Workflow'
80
+ ]
81
+ }
82
+ ];
83
+
84
+ const mockAssets = [
85
+ { id: 1, name: 'OCR_Demo_Screenshot.png', type: 'image', product: 'ocr', subCategory: null, size: '2.4 MB', date: '2024-12-20' },
86
+ { id: 2, name: 'P2P_Workflow_Diagram.pdf', type: 'document', product: 'p2p', subCategory: 'Budget Approval Workflow', size: '1.8 MB', date: '2024-12-19' },
87
+ { id: 3, name: 'Invoice_Processing_Video.mp4', type: 'video', product: 'ocr', subCategory: null, size: '45.2 MB', date: '2024-12-18' },
88
+ { id: 4, name: 'Sales_Order_Infographic.png', type: 'image', product: 'o2c', subCategory: 'Sales Order Workflow', size: '3.1 MB', date: '2024-12-17' },
89
+ { id: 5, name: 'AP_Automation_Brochure.pdf', type: 'document', product: 'p2p', subCategory: 'Accounts Payable Workflow', size: '5.6 MB', date: '2024-12-16' },
90
+ { id: 6, name: 'O2C_Product_Banner.png', type: 'image', product: 'o2c', subCategory: 'Quotation Workflow', size: '1.2 MB', date: '2024-12-15' },
91
+ { id: 7, name: 'Document_Parsing_Demo.png', type: 'image', product: 'ocr', subCategory: null, size: '890 KB', date: '2024-12-14' },
92
+ { id: 8, name: 'PR_Workflow_Guide.pdf', type: 'document', product: 'p2p', subCategory: 'Purchase Request Workflow', size: '2.3 MB', date: '2024-12-13' },
93
+ ];
94
+
95
+ export default function Repository() {
96
+ const [viewMode, setViewMode] = useState('grid');
97
+ const [searchQuery, setSearchQuery] = useState('');
98
+ const [selectedProduct, setSelectedProduct] = useState('all');
99
+ const [expandedProducts, setExpandedProducts] = useState(['ocr', 'p2p', 'o2c']);
100
+ const [selectedAssets, setSelectedAssets] = useState([]);
101
+ const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
102
+ const [dragOver, setDragOver] = useState(false);
103
+
104
+ const toggleProduct = (productId) => {
105
+ setExpandedProducts(prev =>
106
+ prev.includes(productId)
107
+ ? prev.filter(id => id !== productId)
108
+ : [...prev, productId]
109
+ );
110
+ };
111
+
112
+ const filteredAssets = mockAssets.filter(asset => {
113
+ const matchesSearch = asset.name.toLowerCase().includes(searchQuery.toLowerCase());
114
+ const matchesProduct = selectedProduct === 'all' || asset.product === selectedProduct;
115
+ return matchesSearch && matchesProduct;
116
+ });
117
+
118
+ const getAssetsByProduct = (productId) => {
119
+ return mockAssets.filter(asset => asset.product === productId);
120
+ };
121
+
122
+ const getTypeIcon = (type) => {
123
+ switch(type) {
124
+ case 'image': return <Image className="w-5 h-5 text-pink-500" />;
125
+ case 'video': return <Video className="w-5 h-5 text-red-500" />;
126
+ case 'document': return <FileText className="w-5 h-5 text-blue-500" />;
127
+ default: return <File className="w-5 h-5 text-slate-500" />;
128
+ }
129
+ };
130
+
131
+ const getProductColor = (productId) => {
132
+ const product = products.find(p => p.id === productId);
133
+ return product?.color || 'slate';
134
+ };
135
+
136
+ return (
137
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
138
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
139
+ {/* Header */}
140
+ <motion.div
141
+ initial={{ opacity: 0, y: -20 }}
142
+ animate={{ opacity: 1, y: 0 }}
143
+ className="mb-8"
144
+ >
145
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
146
+ <div>
147
+ <h1 className="text-3xl font-bold text-slate-900 tracking-tight">
148
+ Asset Repository
149
+ </h1>
150
+ <p className="text-slate-500 mt-1">
151
+ Manage your marketing materials, screenshots, and documents
152
+ </p>
153
+ </div>
154
+ <Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}>
155
+ <DialogTrigger asChild>
156
+ <Button className="gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
157
+ <Upload className="w-4 h-4" />
158
+ Upload Assets
159
+ </Button>
160
+ </DialogTrigger>
161
+ <DialogContent className="sm:max-w-lg">
162
+ <DialogHeader>
163
+ <DialogTitle>Upload Assets</DialogTitle>
164
+ </DialogHeader>
165
+ <div className="space-y-4 pt-4">
166
+ <div
167
+ className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
168
+ dragOver ? 'border-blue-500 bg-blue-50' : 'border-slate-200 hover:border-slate-300'
169
+ }`}
170
+ onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
171
+ onDragLeave={() => setDragOver(false)}
172
+ onDrop={(e) => { e.preventDefault(); setDragOver(false); }}
173
+ >
174
+ <Upload className="w-10 h-10 text-slate-400 mx-auto mb-3" />
175
+ <p className="text-sm font-medium text-slate-700">
176
+ Drag and drop files here
177
+ </p>
178
+ <p className="text-xs text-slate-500 mt-1">
179
+ or click to browse
180
+ </p>
181
+ <Button variant="outline" size="sm" className="mt-4">
182
+ Browse Files
183
+ </Button>
184
+ </div>
185
+
186
+ <div className="space-y-3">
187
+ <div>
188
+ <Label>Product Category</Label>
189
+ <Select>
190
+ <SelectTrigger className="mt-1.5">
191
+ <SelectValue placeholder="Select a product" />
192
+ </SelectTrigger>
193
+ <SelectContent>
194
+ {products.map(product => (
195
+ <SelectItem key={product.id} value={product.id}>
196
+ {product.name}
197
+ </SelectItem>
198
+ ))}
199
+ </SelectContent>
200
+ </Select>
201
+ </div>
202
+
203
+ <div>
204
+ <Label>Sub-Category (Optional)</Label>
205
+ <Select>
206
+ <SelectTrigger className="mt-1.5">
207
+ <SelectValue placeholder="Select sub-category" />
208
+ </SelectTrigger>
209
+ <SelectContent>
210
+ <SelectItem value="none">None</SelectItem>
211
+ </SelectContent>
212
+ </Select>
213
+ </div>
214
+ </div>
215
+
216
+ <div className="flex justify-end gap-2 pt-4">
217
+ <Button variant="outline" onClick={() => setUploadDialogOpen(false)}>
218
+ Cancel
219
+ </Button>
220
+ <Button className="bg-blue-600 hover:bg-blue-700">
221
+ Upload
222
+ </Button>
223
+ </div>
224
+ </div>
225
+ </DialogContent>
226
+ </Dialog>
227
+ </div>
228
+ </motion.div>
229
+
230
+ <div className="grid lg:grid-cols-4 gap-6">
231
+ {/* Sidebar - Product Categories */}
232
+ <motion.div
233
+ initial={{ opacity: 0, x: -20 }}
234
+ animate={{ opacity: 1, x: 0 }}
235
+ className="lg:col-span-1"
236
+ >
237
+ <Card className="border-0 shadow-lg shadow-slate-200/50 sticky top-8">
238
+ <CardHeader className="pb-3">
239
+ <CardTitle className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
240
+ Product Categories
241
+ </CardTitle>
242
+ </CardHeader>
243
+ <CardContent className="pt-0">
244
+ <div className="space-y-1">
245
+ <button
246
+ onClick={() => setSelectedProduct('all')}
247
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
248
+ selectedProduct === 'all'
249
+ ? 'bg-blue-50 text-blue-700'
250
+ : 'hover:bg-slate-50 text-slate-700'
251
+ }`}
252
+ >
253
+ <FolderOpen className="w-4 h-4" />
254
+ <span className="font-medium text-sm">All Assets</span>
255
+ <Badge variant="secondary" className="ml-auto text-xs">
256
+ {mockAssets.length}
257
+ </Badge>
258
+ </button>
259
+
260
+ {products.map(product => (
261
+ <div key={product.id}>
262
+ <button
263
+ onClick={() => {
264
+ setSelectedProduct(product.id);
265
+ if (product.subCategories.length > 0) {
266
+ toggleProduct(product.id);
267
+ }
268
+ }}
269
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
270
+ selectedProduct === product.id
271
+ ? `bg-${product.color}-50 text-${product.color}-700`
272
+ : 'hover:bg-slate-50 text-slate-700'
273
+ }`}
274
+ >
275
+ {product.subCategories.length > 0 && (
276
+ <ChevronRight className={`w-4 h-4 transition-transform ${
277
+ expandedProducts.includes(product.id) ? 'rotate-90' : ''
278
+ }`} />
279
+ )}
280
+ {product.subCategories.length === 0 && <div className="w-4" />}
281
+ <span className="font-medium text-sm truncate">{product.shortName}</span>
282
+ <Badge variant="secondary" className="ml-auto text-xs">
283
+ {getAssetsByProduct(product.id).length}
284
+ </Badge>
285
+ </button>
286
+
287
+ <AnimatePresence>
288
+ {expandedProducts.includes(product.id) && product.subCategories.length > 0 && (
289
+ <motion.div
290
+ initial={{ height: 0, opacity: 0 }}
291
+ animate={{ height: 'auto', opacity: 1 }}
292
+ exit={{ height: 0, opacity: 0 }}
293
+ className="overflow-hidden"
294
+ >
295
+ <div className="ml-7 pl-3 border-l border-slate-200 mt-1 space-y-1">
296
+ {product.subCategories.map((sub, idx) => (
297
+ <button
298
+ key={idx}
299
+ className="w-full text-left px-3 py-2 text-sm text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-colors"
300
+ >
301
+ {sub}
302
+ </button>
303
+ ))}
304
+ </div>
305
+ </motion.div>
306
+ )}
307
+ </AnimatePresence>
308
+ </div>
309
+ ))}
310
+ </div>
311
+ </CardContent>
312
+ </Card>
313
+ </motion.div>
314
+
315
+ {/* Main Content */}
316
+ <motion.div
317
+ initial={{ opacity: 0, y: 20 }}
318
+ animate={{ opacity: 1, y: 0 }}
319
+ className="lg:col-span-3"
320
+ >
321
+ {/* Search and Filters */}
322
+ <Card className="border-0 shadow-lg shadow-slate-200/50 mb-6">
323
+ <CardContent className="p-4">
324
+ <div className="flex flex-col sm:flex-row gap-3">
325
+ <div className="relative flex-1">
326
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
327
+ <Input
328
+ placeholder="Search assets..."
329
+ value={searchQuery}
330
+ onChange={(e) => setSearchQuery(e.target.value)}
331
+ className="pl-10 border-slate-200"
332
+ />
333
+ </div>
334
+ <div className="flex gap-2">
335
+ <Select defaultValue="all">
336
+ <SelectTrigger className="w-32 border-slate-200">
337
+ <Filter className="w-4 h-4 mr-2" />
338
+ <SelectValue placeholder="Type" />
339
+ </SelectTrigger>
340
+ <SelectContent>
341
+ <SelectItem value="all">All Types</SelectItem>
342
+ <SelectItem value="image">Images</SelectItem>
343
+ <SelectItem value="document">Documents</SelectItem>
344
+ <SelectItem value="video">Videos</SelectItem>
345
+ </SelectContent>
346
+ </Select>
347
+ <div className="flex border border-slate-200 rounded-lg overflow-hidden">
348
+ <Button
349
+ variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
350
+ size="icon"
351
+ onClick={() => setViewMode('grid')}
352
+ className="rounded-none"
353
+ >
354
+ <Grid3X3 className="w-4 h-4" />
355
+ </Button>
356
+ <Button
357
+ variant={viewMode === 'list' ? 'secondary' : 'ghost'}
358
+ size="icon"
359
+ onClick={() => setViewMode('list')}
360
+ className="rounded-none"
361
+ >
362
+ <List className="w-4 h-4" />
363
+ </Button>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ </CardContent>
368
+ </Card>
369
+
370
+ {/* Assets Grid/List */}
371
+ {viewMode === 'grid' ? (
372
+ <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
373
+ {filteredAssets.map((asset, index) => (
374
+ <motion.div
375
+ key={asset.id}
376
+ initial={{ opacity: 0, scale: 0.95 }}
377
+ animate={{ opacity: 1, scale: 1 }}
378
+ transition={{ delay: index * 0.05 }}
379
+ >
380
+ <Card className="border-0 shadow-md hover:shadow-lg transition-all duration-300 group overflow-hidden">
381
+ <div className={`h-40 bg-gradient-to-br from-${getProductColor(asset.product)}-100 to-${getProductColor(asset.product)}-50 flex items-center justify-center relative`}>
382
+ {asset.type === 'image' ? (
383
+ <Image className={`w-12 h-12 text-${getProductColor(asset.product)}-400`} />
384
+ ) : asset.type === 'video' ? (
385
+ <Video className={`w-12 h-12 text-${getProductColor(asset.product)}-400`} />
386
+ ) : (
387
+ <FileText className={`w-12 h-12 text-${getProductColor(asset.product)}-400`} />
388
+ )}
389
+ <div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
390
+ <div className="flex gap-2">
391
+ <Button size="icon" variant="secondary" className="h-9 w-9">
392
+ <Eye className="w-4 h-4" />
393
+ </Button>
394
+ <Button size="icon" variant="secondary" className="h-9 w-9">
395
+ <Download className="w-4 h-4" />
396
+ </Button>
397
+ </div>
398
+ </div>
399
+ <div className="absolute top-2 right-2">
400
+ <Checkbox className="bg-white border-slate-300" />
401
+ </div>
402
+ </div>
403
+ <CardContent className="p-4">
404
+ <div className="flex items-start justify-between gap-2">
405
+ <div className="min-w-0">
406
+ <h3 className="font-medium text-slate-900 text-sm truncate">
407
+ {asset.name}
408
+ </h3>
409
+ <p className="text-xs text-slate-500 mt-1">
410
+ {asset.size} • {asset.date}
411
+ </p>
412
+ </div>
413
+ <DropdownMenu>
414
+ <DropdownMenuTrigger asChild>
415
+ <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0">
416
+ <MoreVertical className="w-4 h-4" />
417
+ </Button>
418
+ </DropdownMenuTrigger>
419
+ <DropdownMenuContent align="end">
420
+ <DropdownMenuItem>
421
+ <Eye className="w-4 h-4 mr-2" /> Preview
422
+ </DropdownMenuItem>
423
+ <DropdownMenuItem>
424
+ <Download className="w-4 h-4 mr-2" /> Download
425
+ </DropdownMenuItem>
426
+ <DropdownMenuItem className="text-red-600">
427
+ <Trash2 className="w-4 h-4 mr-2" /> Delete
428
+ </DropdownMenuItem>
429
+ </DropdownMenuContent>
430
+ </DropdownMenu>
431
+ </div>
432
+ <div className="flex gap-2 mt-3">
433
+ <Badge variant="outline" className={`text-xs border-${getProductColor(asset.product)}-200 text-${getProductColor(asset.product)}-700 bg-${getProductColor(asset.product)}-50`}>
434
+ {products.find(p => p.id === asset.product)?.shortName}
435
+ </Badge>
436
+ {asset.subCategory && (
437
+ <Badge variant="outline" className="text-xs border-slate-200 text-slate-600 truncate max-w-[120px]">
438
+ {asset.subCategory}
439
+ </Badge>
440
+ )}
441
+ </div>
442
+ </CardContent>
443
+ </Card>
444
+ </motion.div>
445
+ ))}
446
+ </div>
447
+ ) : (
448
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
449
+ <CardContent className="p-0">
450
+ <div className="divide-y divide-slate-100">
451
+ {filteredAssets.map((asset, index) => (
452
+ <motion.div
453
+ key={asset.id}
454
+ initial={{ opacity: 0, x: -10 }}
455
+ animate={{ opacity: 1, x: 0 }}
456
+ transition={{ delay: index * 0.03 }}
457
+ className="flex items-center gap-4 p-4 hover:bg-slate-50 transition-colors"
458
+ >
459
+ <Checkbox />
460
+ <div className={`w-10 h-10 rounded-lg bg-${getProductColor(asset.product)}-100 flex items-center justify-center`}>
461
+ {getTypeIcon(asset.type)}
462
+ </div>
463
+ <div className="flex-1 min-w-0">
464
+ <h3 className="font-medium text-slate-900 text-sm truncate">
465
+ {asset.name}
466
+ </h3>
467
+ <p className="text-xs text-slate-500 mt-0.5">
468
+ {asset.subCategory || products.find(p => p.id === asset.product)?.name}
469
+ </p>
470
+ </div>
471
+ <div className="hidden sm:block text-sm text-slate-500">
472
+ {asset.size}
473
+ </div>
474
+ <div className="hidden md:block text-sm text-slate-500">
475
+ {asset.date}
476
+ </div>
477
+ <Badge variant="outline" className={`text-xs border-${getProductColor(asset.product)}-200 text-${getProductColor(asset.product)}-700`}>
478
+ {products.find(p => p.id === asset.product)?.shortName}
479
+ </Badge>
480
+ <DropdownMenu>
481
+ <DropdownMenuTrigger asChild>
482
+ <Button variant="ghost" size="icon" className="h-8 w-8">
483
+ <MoreVertical className="w-4 h-4" />
484
+ </Button>
485
+ </DropdownMenuTrigger>
486
+ <DropdownMenuContent align="end">
487
+ <DropdownMenuItem>
488
+ <Eye className="w-4 h-4 mr-2" /> Preview
489
+ </DropdownMenuItem>
490
+ <DropdownMenuItem>
491
+ <Download className="w-4 h-4 mr-2" /> Download
492
+ </DropdownMenuItem>
493
+ <DropdownMenuItem className="text-red-600">
494
+ <Trash2 className="w-4 h-4 mr-2" /> Delete
495
+ </DropdownMenuItem>
496
+ </DropdownMenuContent>
497
+ </DropdownMenu>
498
+ </motion.div>
499
+ ))}
500
+ </div>
501
+ </CardContent>
502
+ </Card>
503
+ )}
504
+ </motion.div>
505
+ </div>
506
+ </div>
507
+ </div>
508
+ );
509
+ }
510
+
frontend/src/pages/Scheduler.jsx ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Calendar as CalendarIcon,
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ Plus,
8
+ Clock,
9
+ Layers,
10
+ Image,
11
+ FileText,
12
+ Video,
13
+ Filter,
14
+ Settings,
15
+ Sparkles,
16
+ Check,
17
+ X,
18
+ GripVertical
19
+ } from 'lucide-react';
20
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Badge } from '@/components/ui/badge';
23
+ import { Calendar } from '@/components/ui/calendar';
24
+ import { Checkbox } from '@/components/ui/checkbox';
25
+ import { Label } from '@/components/ui/label';
26
+ import { Slider } from '@/components/ui/slider';
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue,
33
+ } from '@/components/ui/select';
34
+ import {
35
+ Dialog,
36
+ DialogContent,
37
+ DialogHeader,
38
+ DialogTitle,
39
+ DialogTrigger,
40
+ } from '@/components/ui/dialog';
41
+ import {
42
+ Popover,
43
+ PopoverContent,
44
+ PopoverTrigger,
45
+ } from '@/components/ui/popover';
46
+ import { format, addDays, startOfWeek, endOfWeek, eachDayOfInterval, isSameDay, isToday } from 'date-fns';
47
+ import { Link } from 'react-router-dom';
48
+ import { createPageUrl } from '@/utils';
49
+
50
+ const products = [
51
+ { id: 'ocr', name: 'Document Parsing (OCR)', shortName: 'OCR', color: 'blue' },
52
+ { id: 'p2p', name: 'Purchase To Pay', shortName: 'P2P', color: 'emerald', subCategories: ['Budget Approval', 'Purchase Request', 'Accounts Payable'] },
53
+ { id: 'o2c', name: 'Order to Cash', shortName: 'O2C', color: 'violet', subCategories: ['Quotation', 'Sales Order', 'PickSlip Delivery', 'Accounts Receivable'] },
54
+ ];
55
+
56
+ const postTypes = [
57
+ { id: 'carousel', name: 'Carousel', icon: Layers, description: 'Multi-slide visual story' },
58
+ { id: 'cover_content', name: 'Cover Image + Content', icon: Image, description: 'Featured image with text' },
59
+ { id: 'content_only', name: 'Content Only', icon: FileText, description: 'Text-based post' },
60
+ { id: 'webinar', name: 'Webinar Invite', icon: Video, description: 'Event promotion' },
61
+ ];
62
+
63
+ const scheduledPosts = [
64
+ { id: 1, title: 'OCR Automation Benefits', type: 'carousel', product: 'ocr', time: '10:00 AM', date: new Date(), status: 'scheduled' },
65
+ { id: 2, title: 'P2P Efficiency Guide', type: 'cover_content', product: 'p2p', time: '2:00 PM', date: new Date(), status: 'draft' },
66
+ { id: 3, title: 'O2C Webinar', type: 'webinar', product: 'o2c', time: '11:00 AM', date: addDays(new Date(), 1), status: 'scheduled' },
67
+ { id: 4, title: 'Invoice Processing Tips', type: 'content_only', product: 'ocr', time: '3:00 PM', date: addDays(new Date(), 1), status: 'scheduled' },
68
+ { id: 5, title: 'Budget Approval Flow', type: 'carousel', product: 'p2p', time: '9:00 AM', date: addDays(new Date(), 2), status: 'scheduled' },
69
+ { id: 6, title: 'Sales Order Demo', type: 'cover_content', product: 'o2c', time: '1:00 PM', date: addDays(new Date(), 3), status: 'draft' },
70
+ ];
71
+
72
+ export default function Scheduler() {
73
+ const [selectedDate, setSelectedDate] = useState(new Date());
74
+ const [currentWeekStart, setCurrentWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 }));
75
+ const [campaignDialogOpen, setCampaignDialogOpen] = useState(false);
76
+ const [selectedProducts, setSelectedProducts] = useState(['ocr', 'p2p', 'o2c']);
77
+ const [selectedPostTypes, setSelectedPostTypes] = useState(['carousel', 'cover_content']);
78
+ const [dateRange, setDateRange] = useState({ from: new Date(), to: addDays(new Date(), 30) });
79
+ const [postsPerWeek, setPostsPerWeek] = useState([5]);
80
+
81
+ const weekDays = eachDayOfInterval({
82
+ start: currentWeekStart,
83
+ end: endOfWeek(currentWeekStart, { weekStartsOn: 1 })
84
+ });
85
+
86
+ const navigateWeek = (direction) => {
87
+ setCurrentWeekStart(prev => addDays(prev, direction === 'next' ? 7 : -7));
88
+ };
89
+
90
+ const getPostsForDate = (date) => {
91
+ return scheduledPosts.filter(post => isSameDay(post.date, date));
92
+ };
93
+
94
+ const getProductColor = (productId) => {
95
+ const product = products.find(p => p.id === productId);
96
+ return product?.color || 'slate';
97
+ };
98
+
99
+ const getPostTypeIcon = (typeId) => {
100
+ const type = postTypes.find(t => t.id === typeId);
101
+ return type?.icon || FileText;
102
+ };
103
+
104
+ const toggleProduct = (productId) => {
105
+ setSelectedProducts(prev =>
106
+ prev.includes(productId)
107
+ ? prev.filter(id => id !== productId)
108
+ : [...prev, productId]
109
+ );
110
+ };
111
+
112
+ const togglePostType = (typeId) => {
113
+ setSelectedPostTypes(prev =>
114
+ prev.includes(typeId)
115
+ ? prev.filter(id => id !== typeId)
116
+ : [...prev, typeId]
117
+ );
118
+ };
119
+
120
+ return (
121
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
122
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
123
+ {/* Header */}
124
+ <motion.div
125
+ initial={{ opacity: 0, y: -20 }}
126
+ animate={{ opacity: 1, y: 0 }}
127
+ className="mb-8"
128
+ >
129
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
130
+ <div>
131
+ <h1 className="text-3xl font-bold text-slate-900 tracking-tight">
132
+ Content Scheduler
133
+ </h1>
134
+ <p className="text-slate-500 mt-1">
135
+ Plan and schedule your LinkedIn content calendar
136
+ </p>
137
+ </div>
138
+ <div className="flex gap-3">
139
+ <Dialog open={campaignDialogOpen} onOpenChange={setCampaignDialogOpen}>
140
+ <DialogTrigger asChild>
141
+ <Button variant="outline" className="gap-2 border-slate-200">
142
+ <Settings className="w-4 h-4" />
143
+ Campaign Settings
144
+ </Button>
145
+ </DialogTrigger>
146
+ <DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
147
+ <DialogHeader>
148
+ <DialogTitle className="flex items-center gap-2">
149
+ <Sparkles className="w-5 h-5 text-amber-500" />
150
+ Configure Campaign
151
+ </DialogTitle>
152
+ </DialogHeader>
153
+ <div className="space-y-6 pt-4">
154
+ {/* Date Range */}
155
+ <div className="space-y-3">
156
+ <Label className="text-sm font-semibold">Campaign Date Range</Label>
157
+ <div className="grid grid-cols-2 gap-4">
158
+ <div>
159
+ <Label className="text-xs text-slate-500">Start Date</Label>
160
+ <Popover>
161
+ <PopoverTrigger asChild>
162
+ <Button variant="outline" className="w-full justify-start mt-1.5">
163
+ <CalendarIcon className="w-4 h-4 mr-2" />
164
+ {format(dateRange.from, 'MMM d, yyyy')}
165
+ </Button>
166
+ </PopoverTrigger>
167
+ <PopoverContent className="w-auto p-0" align="start">
168
+ <Calendar
169
+ mode="single"
170
+ selected={dateRange.from}
171
+ onSelect={(date) => setDateRange(prev => ({ ...prev, from: date }))}
172
+ />
173
+ </PopoverContent>
174
+ </Popover>
175
+ </div>
176
+ <div>
177
+ <Label className="text-xs text-slate-500">End Date</Label>
178
+ <Popover>
179
+ <PopoverTrigger asChild>
180
+ <Button variant="outline" className="w-full justify-start mt-1.5">
181
+ <CalendarIcon className="w-4 h-4 mr-2" />
182
+ {format(dateRange.to, 'MMM d, yyyy')}
183
+ </Button>
184
+ </PopoverTrigger>
185
+ <PopoverContent className="w-auto p-0" align="start">
186
+ <Calendar
187
+ mode="single"
188
+ selected={dateRange.to}
189
+ onSelect={(date) => setDateRange(prev => ({ ...prev, to: date }))}
190
+ />
191
+ </PopoverContent>
192
+ </Popover>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ {/* Products to Focus */}
198
+ <div className="space-y-3">
199
+ <Label className="text-sm font-semibold">Products to Focus</Label>
200
+ <div className="grid gap-3">
201
+ {products.map(product => (
202
+ <div key={product.id} className="space-y-2">
203
+ <div
204
+ onClick={() => toggleProduct(product.id)}
205
+ className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${
206
+ selectedProducts.includes(product.id)
207
+ ? `border-${product.color}-300 bg-${product.color}-50`
208
+ : 'border-slate-200 hover:border-slate-300'
209
+ }`}
210
+ >
211
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
212
+ selectedProducts.includes(product.id)
213
+ ? `bg-${product.color}-500 text-white`
214
+ : 'bg-slate-100 text-slate-400'
215
+ }`}>
216
+ {selectedProducts.includes(product.id) ? (
217
+ <Check className="w-4 h-4" />
218
+ ) : (
219
+ <Plus className="w-4 h-4" />
220
+ )}
221
+ </div>
222
+ <div className="flex-1">
223
+ <p className="font-medium text-slate-900">{product.name}</p>
224
+ {product.subCategories && (
225
+ <p className="text-xs text-slate-500 mt-0.5">
226
+ {product.subCategories.join(', ')}
227
+ </p>
228
+ )}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ </div>
235
+
236
+ {/* Post Types Mix */}
237
+ <div className="space-y-3">
238
+ <Label className="text-sm font-semibold">Post Types Mix</Label>
239
+ <div className="grid grid-cols-2 gap-3">
240
+ {postTypes.map(type => (
241
+ <div
242
+ key={type.id}
243
+ onClick={() => togglePostType(type.id)}
244
+ className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${
245
+ selectedPostTypes.includes(type.id)
246
+ ? 'border-blue-300 bg-blue-50'
247
+ : 'border-slate-200 hover:border-slate-300'
248
+ }`}
249
+ >
250
+ <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
251
+ selectedPostTypes.includes(type.id)
252
+ ? 'bg-blue-500 text-white'
253
+ : 'bg-slate-100 text-slate-400'
254
+ }`}>
255
+ <type.icon className="w-5 h-5" />
256
+ </div>
257
+ <div>
258
+ <p className="font-medium text-sm text-slate-900">{type.name}</p>
259
+ <p className="text-xs text-slate-500">{type.description}</p>
260
+ </div>
261
+ </div>
262
+ ))}
263
+ </div>
264
+ </div>
265
+
266
+ {/* Posting Frequency */}
267
+ <div className="space-y-3">
268
+ <div className="flex items-center justify-between">
269
+ <Label className="text-sm font-semibold">Posts per Week</Label>
270
+ <span className="text-lg font-bold text-blue-600">{postsPerWeek[0]}</span>
271
+ </div>
272
+ <Slider
273
+ value={postsPerWeek}
274
+ onValueChange={setPostsPerWeek}
275
+ min={1}
276
+ max={14}
277
+ step={1}
278
+ className="py-4"
279
+ />
280
+ <div className="flex justify-between text-xs text-slate-500">
281
+ <span>1 post</span>
282
+ <span>14 posts</span>
283
+ </div>
284
+ </div>
285
+
286
+ <div className="flex justify-end gap-2 pt-4 border-t">
287
+ <Button variant="outline" onClick={() => setCampaignDialogOpen(false)}>
288
+ Cancel
289
+ </Button>
290
+ <Button className="bg-gradient-to-r from-blue-600 to-indigo-600 gap-2">
291
+ <Sparkles className="w-4 h-4" />
292
+ Generate Schedule
293
+ </Button>
294
+ </div>
295
+ </div>
296
+ </DialogContent>
297
+ </Dialog>
298
+
299
+ <Link to={createPageUrl('PostEditor')}>
300
+ <Button className="gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
301
+ <Plus className="w-4 h-4" />
302
+ Create Post
303
+ </Button>
304
+ </Link>
305
+ </div>
306
+ </div>
307
+ </motion.div>
308
+
309
+ <div className="grid lg:grid-cols-4 gap-6">
310
+ {/* Calendar Sidebar */}
311
+ <motion.div
312
+ initial={{ opacity: 0, x: -20 }}
313
+ animate={{ opacity: 1, x: 0 }}
314
+ className="lg:col-span-1 space-y-6"
315
+ >
316
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
317
+ <CardContent className="p-4">
318
+ <Calendar
319
+ mode="single"
320
+ selected={selectedDate}
321
+ onSelect={setSelectedDate}
322
+ className="rounded-md"
323
+ />
324
+ </CardContent>
325
+ </Card>
326
+
327
+ {/* Quick Stats */}
328
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
329
+ <CardHeader className="pb-3">
330
+ <CardTitle className="text-sm font-semibold text-slate-600">This Week</CardTitle>
331
+ </CardHeader>
332
+ <CardContent className="space-y-3">
333
+ <div className="flex items-center justify-between">
334
+ <span className="text-sm text-slate-600">Scheduled</span>
335
+ <Badge className="bg-blue-100 text-blue-700 border-0">12 posts</Badge>
336
+ </div>
337
+ <div className="flex items-center justify-between">
338
+ <span className="text-sm text-slate-600">Drafts</span>
339
+ <Badge className="bg-amber-100 text-amber-700 border-0">4 posts</Badge>
340
+ </div>
341
+ <div className="flex items-center justify-between">
342
+ <span className="text-sm text-slate-600">Published</span>
343
+ <Badge className="bg-emerald-100 text-emerald-700 border-0">8 posts</Badge>
344
+ </div>
345
+ </CardContent>
346
+ </Card>
347
+
348
+ {/* Product Filter */}
349
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
350
+ <CardHeader className="pb-3">
351
+ <CardTitle className="text-sm font-semibold text-slate-600 flex items-center gap-2">
352
+ <Filter className="w-4 h-4" />
353
+ Filter by Product
354
+ </CardTitle>
355
+ </CardHeader>
356
+ <CardContent className="space-y-2">
357
+ {products.map(product => (
358
+ <label key={product.id} className="flex items-center gap-3 cursor-pointer">
359
+ <Checkbox defaultChecked />
360
+ <span className="text-sm text-slate-700">{product.shortName}</span>
361
+ <div className={`w-2 h-2 rounded-full bg-${product.color}-500 ml-auto`} />
362
+ </label>
363
+ ))}
364
+ </CardContent>
365
+ </Card>
366
+ </motion.div>
367
+
368
+ {/* Week View */}
369
+ <motion.div
370
+ initial={{ opacity: 0, y: 20 }}
371
+ animate={{ opacity: 1, y: 0 }}
372
+ className="lg:col-span-3"
373
+ >
374
+ <Card className="border-0 shadow-lg shadow-slate-200/50">
375
+ <CardHeader className="pb-4 border-b border-slate-100">
376
+ <div className="flex items-center justify-between">
377
+ <CardTitle className="text-lg font-semibold">
378
+ {format(currentWeekStart, 'MMMM yyyy')}
379
+ </CardTitle>
380
+ <div className="flex items-center gap-2">
381
+ <Button
382
+ variant="outline"
383
+ size="icon"
384
+ onClick={() => navigateWeek('prev')}
385
+ className="h-8 w-8"
386
+ >
387
+ <ChevronLeft className="w-4 h-4" />
388
+ </Button>
389
+ <Button
390
+ variant="outline"
391
+ size="sm"
392
+ onClick={() => setCurrentWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 }))}
393
+ >
394
+ Today
395
+ </Button>
396
+ <Button
397
+ variant="outline"
398
+ size="icon"
399
+ onClick={() => navigateWeek('next')}
400
+ className="h-8 w-8"
401
+ >
402
+ <ChevronRight className="w-4 h-4" />
403
+ </Button>
404
+ </div>
405
+ </div>
406
+ </CardHeader>
407
+ <CardContent className="p-0">
408
+ <div className="grid grid-cols-7 border-b border-slate-100">
409
+ {weekDays.map((day, index) => (
410
+ <div
411
+ key={index}
412
+ className={`text-center py-4 border-r last:border-r-0 border-slate-100 ${
413
+ isToday(day) ? 'bg-blue-50' : ''
414
+ }`}
415
+ >
416
+ <p className="text-xs text-slate-500 uppercase">
417
+ {format(day, 'EEE')}
418
+ </p>
419
+ <p className={`text-lg font-semibold mt-1 ${
420
+ isToday(day) ? 'text-blue-600' : 'text-slate-900'
421
+ }`}>
422
+ {format(day, 'd')}
423
+ </p>
424
+ </div>
425
+ ))}
426
+ </div>
427
+ <div className="grid grid-cols-7 min-h-[400px]">
428
+ {weekDays.map((day, dayIndex) => {
429
+ const dayPosts = getPostsForDate(day);
430
+ return (
431
+ <div
432
+ key={dayIndex}
433
+ className={`border-r last:border-r-0 border-slate-100 p-2 ${
434
+ isToday(day) ? 'bg-blue-50/30' : ''
435
+ }`}
436
+ >
437
+ <AnimatePresence>
438
+ {dayPosts.map((post, postIndex) => {
439
+ const PostIcon = getPostTypeIcon(post.type);
440
+ const color = getProductColor(post.product);
441
+ return (
442
+ <motion.div
443
+ key={post.id}
444
+ initial={{ opacity: 0, scale: 0.9 }}
445
+ animate={{ opacity: 1, scale: 1 }}
446
+ exit={{ opacity: 0, scale: 0.9 }}
447
+ transition={{ delay: postIndex * 0.05 }}
448
+ >
449
+ <Link to={createPageUrl('PostEditor') + `?id=${post.id}`}>
450
+ <div className={`mb-2 p-2 rounded-lg bg-white border border-${color}-200 hover:border-${color}-300 shadow-sm hover:shadow-md transition-all cursor-pointer group`}>
451
+ <div className="flex items-center gap-1.5 mb-1">
452
+ <div className={`w-5 h-5 rounded flex items-center justify-center bg-${color}-100`}>
453
+ <PostIcon className={`w-3 h-3 text-${color}-600`} />
454
+ </div>
455
+ <span className="text-[10px] text-slate-500">{post.time}</span>
456
+ </div>
457
+ <p className="text-xs font-medium text-slate-800 line-clamp-2 leading-tight">
458
+ {post.title}
459
+ </p>
460
+ <div className="flex items-center justify-between mt-1.5">
461
+ <Badge className={`text-[9px] px-1 py-0 bg-${color}-100 text-${color}-700 border-0`}>
462
+ {products.find(p => p.id === post.product)?.shortName}
463
+ </Badge>
464
+ {post.status === 'draft' && (
465
+ <span className="text-[9px] text-amber-600">Draft</span>
466
+ )}
467
+ </div>
468
+ </div>
469
+ </Link>
470
+ </motion.div>
471
+ );
472
+ })}
473
+ </AnimatePresence>
474
+ <Button
475
+ variant="ghost"
476
+ size="sm"
477
+ className="w-full h-7 text-xs text-slate-400 hover:text-slate-600 opacity-0 hover:opacity-100"
478
+ >
479
+ <Plus className="w-3 h-3 mr-1" />
480
+ Add
481
+ </Button>
482
+ </div>
483
+ );
484
+ })}
485
+ </div>
486
+ </CardContent>
487
+ </Card>
488
+ </motion.div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ );
493
+ }
frontend/src/utils.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function createPageUrl(pageName) {
9
+ const routes = {
10
+ Dashboard: '/',
11
+ Repository: '/repository',
12
+ Scheduler: '/scheduler',
13
+ PostEditor: '/post-editor',
14
+ Integrations: '/integrations',
15
+ };
16
+ return routes[pageName] || '/';
17
+ }
18
+
frontend/tailwind.config.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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/vite.config.js CHANGED
@@ -1,9 +1,15 @@
1
  import { defineConfig } from "vite";
2
  import react from "@vitejs/plugin-react";
 
3
 
4
  // IMPORTANT: proxy API calls to backend when running both inside one container
5
  export default defineConfig({
6
  plugins: [react()],
 
 
 
 
 
7
  server: {
8
  proxy: {
9
  "/api": "http://127.0.0.1:8000"
 
1
  import { defineConfig } from "vite";
2
  import react from "@vitejs/plugin-react";
3
+ import path from "path";
4
 
5
  // IMPORTANT: proxy API calls to backend when running both inside one container
6
  export default defineConfig({
7
  plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
  server: {
14
  proxy: {
15
  "/api": "http://127.0.0.1:8000"