This commit is contained in:
DivyamAgg24 2025-05-19 12:21:54 +05:30
parent 6be828a796
commit 3ff133c561
108 changed files with 29163 additions and 1211 deletions

View File

@ -1,84 +1,3 @@
# Turborepo starter
# Introduction
This Turborepo starter is maintained by the Turborepo core team.
## Using this example
Run the following command:
```sh
npx create-turbo@latest
```
## What's inside?
This Turborepo includes the following packages/apps:
### Apps and Packages
- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
### Utilities
This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
pnpm build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
pnpm dev
```
### Remote Caching
> [!TIP]
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks)
- [Caching](https://turborepo.com/docs/crafting-your-repository/caching)
- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching)
- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters)
- [Configuration Options](https://turborepo.com/docs/reference/configuration)
- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference)
This project is built for creators. They can create, edit and manage their ideas and content. The website also has built in AI that helps in creating new ideas and generating content

150
apps/backend/auth.ts Normal file
View File

@ -0,0 +1,150 @@
import express from "express";
import z from "zod";
import jwt from "jsonwebtoken";
import prisma from "@repo/db/client";
import JWT_SECRET from "./config.js";
const router = express.Router();
const signupBody = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string(),
});
router.post("/register", async (req, res) => {
try {
const validation = signupBody.safeParse(req.body);
if (!validation.success) {
res.status(411).json({
message: "Incorrect input format",
errors: validation.error.format()
});
return;
}
const existingUser = await prisma.user.findUnique({
where: {
email: req.body.email
}
});
if (existingUser) {
res.status(409).json({
message: "Email already taken"
});
return;
}
const user = await prisma.user.create({
data: {
name: req.body.name,
email: req.body.email,
password: req.body.password
}
});
const token = jwt.sign(
{
userId: user.id,
userEmail: user.email
},
JWT_SECRET,
{ expiresIn: '24h' }
);
req.headers.authorization = token;
res.status(201).json({
message: "User created successfully",
token: token,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
return;
} catch (error: any) {
console.error("Registration error:", error);
if (error.code === 'P2002') {
res.status(409).json({
message: "Email already exists"
});
return;
}
res.status(500).json({
message: "Server error while processing registration"
});
return;
}
});
const loginBody = z.object({
email: z.string().email(),
password: z.string(),
});
router.post("/login", async (req, res) => {
try {
const validation = loginBody.safeParse(req.body);
if (!validation.success) {
res.status(411).json({
message: "Incorrect inputs",
errors: validation.error.format()
})
return;
}
const userFound = await prisma.user.findUnique({
where: {
email: req.body.email,
password: req.body.password
}
});
if (!userFound) {
res.status(401).json({
message: "Invalid email or password"
});
return;
}
const token = jwt.sign(
{
userId: userFound.id,
userEmail: userFound.email
},
JWT_SECRET,
{ expiresIn: '24h' }
);
req.headers.authorization = token
res.json({
success: true,
token: token,
user: {
id: userFound.id,
email: userFound.email,
name: userFound.name
}
});
return;
} catch (error) {
console.error("Login error:", error);
res.status(500).json({
message: "Server error while processing login"
});
return;
}
});
export default router;

2
apps/backend/config.ts Normal file
View File

@ -0,0 +1,2 @@
const JWT_SECRET = "creatorHub_jwt_secret"
export default JWT_SECRET

81
apps/backend/constant.ts Normal file
View File

@ -0,0 +1,81 @@
export const SystemPrompt = `You are a specialized content strategist AI for a cross-platform idea management tool. Your purpose is to help users develop structured, platform-optimized content from their ideas.
When given a content idea or topic, generate platform-specific content in a structured JSON format that can be easily parsed by the frontend. If no specific platforms are mentioned, provide recommendations for Instagram, Facebook, Twitter, LinkedIn, and YouTube.
IMPORTANT: Always respond with clean, parseable JSON only. Return the raw JSON without any backticks, code block markers, or the word "json". Do not include any introductory text, conclusions, or explanations outside the JSON structure.
Use the following output format:
{
"platforms": {
"instagram": {
"title": "",
"description": "",
"hashtags": [],
"callToAction": "",
"bestPostingTimes": "",
"contentFormat": "",
"additionalTips": ""
},
// Other platforms following the same structure
},
"generalTips": []
}
Platform-specific considerations:
1. Instagram:
- title: Short, attention-grabbing title
- description: Caption under 2,200 characters
- hashtags: 3-5 relevant hashtags (without # symbol)
- callToAction: Question or prompt to increase engagement
- bestPostingTimes: Optimal posting windows
- contentFormat: Recommended format (Reel, Carousel, Single Image, etc.)
- Additional fields: Any platform-specific details
2. Facebook:
- title: Headline for post
- description: Longer-form description
- hashtags: 1-2 relevant hashtags (without # symbol)
- callToAction: Question or engagement prompt
- bestPostingTimes: Optimal posting windows
- contentFormat: Recommended format (Video, Image, Text, etc.)
- Additional fields: Any platform-specific details
3. Twitter/X:
- title: N/A or very brief intro
- description: Concise post under 280 characters
- hashtags: 1-2 hashtags (without # symbol)
- callToAction: Question or engagement prompt
- bestPostingTimes: Optimal posting windows
- contentFormat: Text, Image, or Video
- additionalTips: Thread structure if needed
4. LinkedIn:
- title: Professional headline
- description: Structured content with 3-5 paragraphs
- hashtags: 3-5 industry hashtags (without # symbol)
- callToAction: Professional engagement prompt
- bestPostingTimes: Business hours recommendation
- contentFormat: Text post, Article, or Document
- additionalTips: Any platform-specific details
5. YouTube:
- title: Video title under 100 characters
- description: Full description with timestamps and links
- hashtags: Up to 15 relevant hashtags (without # symbol)
- callToAction: Subscription or engagement request
- bestPostingTimes: Optimal posting times
- contentFormat: Short/Long video, specifications
- additionalTips: Info about end screens, cards, etc.
If the user asks for content for a specific topic like "How to be productive," provide detailed, ready-to-use content for each requested platform. If the user wants a Reel idea specifically, focus on providing a detailed structure for Instagram Reels in the contentDetails field.
For visual content (Reels, TikTok, YouTube), include a "contentDetails" field with scene-by-scene breakdown, including:
- scenes: Array of scene objects with:
- description: What happens in the scene
- text: Text overlay for this scene
- duration: Approximate duration in seconds
- audio: Suggested audio type/track
- transitions: Suggested transitions between scenes
IMPORTANT: Return only raw JSON without any formatting markers or wrappers. Do not include the word 'json' or any code formatting markers like backticks in your response. Do not add comments within the JSON that would make it invalid.`

190
apps/backend/events.ts Normal file
View File

@ -0,0 +1,190 @@
import express from "express";
import { z } from "zod";
import prisma from "@repo/db/client";
import authMiddleware from "./middleware";
const router = express.Router();
const eventSchema = z.object({
title: z.string().min(1, "Title is required"),
start: z.string().datetime(),
end: z.string().datetime(),
allDay: z.boolean(),
description: z.string().optional(),
color: z.string().optional(),
});
router.get("/getEvents", authMiddleware, async (req: any, res) => {
try {
const eventList = await prisma.event.findMany({
where: {
userId: req.userId
},
orderBy: {
start: "asc"
}
})
res.status(200).json({
success: true,
data: eventList
})
return;
} catch (e) {
console.error("Error fetching events:", e);
res.status(500).json({
success: false,
message: "Failed to fetch events"
})
return
}
})
router.post("/createEvent", authMiddleware, async (req: any, res) => {
try {
const validationResult = eventSchema.safeParse(req.body)
if (!validationResult.success) {
res.status(400).json({
success: false,
message: "Invalid event data",
errors: validationResult.error.errors,
});
return;
}
const { title, start, end, allDay, description, color } = validationResult.data
const newEvent = await prisma.event.create({
data: {
title,
start,
end,
allDay,
description,
color,
userId: req.userId
}
})
res.status(201).json({
success: true,
data: newEvent,
});
return
} catch (e) {
console.error("Error creating event:", e);
res.status(500).json({
success: false,
message: "Failed to create event",
});
return
}
})
router.put("/updateEvent/:id", authMiddleware, async (req: any, res) => {
try {
const { id } = req.params;
const validationResult = eventSchema.safeParse(req.body);
if (!validationResult.success) {
res.status(400).json({
success: false,
message: "Invalid event data",
errors: validationResult.error.errors,
});
return
}
const existingEvent = await prisma.event.findUnique({
where: {
id,
userId: req.userId,
},
});
if (!existingEvent) {
res.status(404).json({
success: false,
message: "Event not found or you don't have permission to update it",
});
return
}
const { title, start, end, allDay, description, color } = validationResult.data;
const updatedEvent = await prisma.event.update({
where: {
id,
},
data: {
title,
start,
end,
allDay,
description,
color,
updatedAt: new Date(),
},
});
res.status(200).json({
success: true,
data: updatedEvent,
});
return
} catch (error) {
console.error("Error updating event:", error);
res.status(500).json({
success: false,
message: "Failed to update event",
});
return
}
});
// DELETE an event
router.delete("/deleteEvent/:id", authMiddleware, async (req: any, res) => {
try {
const { id } = req.params;
// Check if the event exists and belongs to this user
const existingEvent = await prisma.event.findUnique({
where: {
id,
userId: req.userId,
},
});
if (!existingEvent) {
res.status(404).json({
success: false,
message: "Event not found or you don't have permission to delete it",
});
return;
}
await prisma.event.delete({
where: {
id,
},
});
res.status(200).json({
success: true,
message: "Event deleted successfully",
});
return;
} catch (error) {
console.error("Error deleting event:", error);
res.status(500).json({
success: false,
message: "Failed to delete event",
});
return;
}
});
export default router
"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"

329
apps/backend/ideas.ts Normal file
View File

@ -0,0 +1,329 @@
import express from "express";
import { z } from "zod";
import prisma from "@repo/db/client";
import authMiddleware from "./middleware";
import { GoogleGenAI } from '@google/genai';
import {SystemPrompt} from "./constant"
const router = express.Router();
// Schema validation for idea creation and updates
const ideaSchema = z.object({
title: z.string().min(1, "Title is required"),
status: z.string().min(1, "Status is required"),
tags: z.array(z.string()),
content: z.array(z.any()), // Allowing any for Slate's Descendant[] type
platformContent: z.any().optional()
});
// GET all ideas for the authenticated user
router.get("/getIdeas", authMiddleware, async (req: any, res) => {
try {
const ideaList = await prisma.idea.findMany({
where: {
userId: req.userId,
},
orderBy: {
updatedAt: "desc", // Most recently updated first
},
});
res.status(200).json({
success: true,
data: ideaList,
});
return
} catch (error) {
console.error("Error fetching ideas:", error);
res.status(500).json({
success: false,
message: "Failed to fetch ideas",
});
return
}
});
router.post("/createIdea", authMiddleware, async (req: any, res) => {
try {
const validationResult = ideaSchema.safeParse(req.body);
if (!validationResult.success) {
res.status(400).json({
success: false,
message: "Invalid idea data",
errors: validationResult.error.errors,
});
return;
}
const { title, status, tags, content, platformContent } = validationResult.data;
const newIdea = await prisma.idea.create({
data: {
title,
status,
tags,
content, // Store Slate's content directly
platformContent,
userId: req.userId,
},
});
res.status(201).json({
success: true,
data: newIdea,
});
return
} catch (error) {
console.error("Error creating idea:", error);
res.status(500).json({
success: false,
message: "Failed to create idea",
});
return
}
});
const promptSchema = z.object({
prompt: z.string().min(1, "Prompt is required")
})
router.post("/AIIdeaContent", authMiddleware, async (req: any, res: any) => {
try {
const validationResult = promptSchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
success: false,
message: "Invalid prompt data",
errors: validationResult.error.errors,
});
}
const { prompt } = validationResult.data;
// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Create Google Gemini AI instance
const ai = new GoogleGenAI({
apiKey: "AIzaSyAGXspPFiKkmicShhkyuGnUvqxuaWbBtKE",
});
const config = {
responseMimeType: 'text/plain',
systemInstruction: [
{
text: SystemPrompt
}
],
};
const model = 'gemini-2.0-flash';
const contents = [
{
role: 'user',
parts: [
{
text: prompt,
},
],
},
];
try {
const response = await ai.models.generateContentStream({
model,
config,
contents,
});
// Stream each chunk to the client
for await (const chunk of response) {
// Send the chunk as a Server-Sent Event
if (chunk.text) {
console.log(chunk.text)
res.write(`data: ${JSON.stringify({ text: chunk.text })}\n\n`);
// Flush the data immediately
if (res.flush) {
res.flush();
}
}
}
// End the stream
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
} catch (aiError) {
console.error("AI generation error:", aiError);
res.write(`data: ${JSON.stringify({ error: "AI generation failed" })}\n\n`);
res.end();
}
} catch (error) {
console.error("Error in AI content generation:", error);
if (!res.headersSent) {
res.status(500).json({
success: false,
message: "Failed to generate AI content",
});
} else {
res.write(`data: ${JSON.stringify({ error: "Failed to generate AI content" })}\n\n`);
res.end();
}
}
});
// PUT update an existing idea
router.put("/updateIdea/:id", authMiddleware, async (req: any, res) => {
try {
const { id } = req.params;
const validationResult = ideaSchema.safeParse(req.body);
if (!validationResult.success) {
res.status(400).json({
success: false,
message: "Invalid idea data",
errors: validationResult.error.errors,
});
return
}
// First check if the idea exists and belongs to this user
const existingIdea = await prisma.idea.findUnique({
where: {
id,
userId: req.userId,
},
});
if (!existingIdea) {
res.status(404).json({
success: false,
message: "Idea not found or you don't have permission to update it",
});
return
}
const { title, status, tags, content, platformContent } = validationResult.data;
const updatedIdea = await prisma.idea.update({
where: {
id,
},
data: {
title,
status,
tags,
content,
platformContent,
updatedAt: new Date(),
},
});
res.status(200).json({
success: true,
data: updatedIdea,
});
return
} catch (error) {
console.error("Error updating idea:", error);
res.status(500).json({
success: false,
message: "Failed to update idea",
});
return
}
});
// DELETE an idea
router.delete("/deleteIdea/:id", authMiddleware, async (req: any, res) => {
try {
const { id } = req.params;
// Check if the idea exists and belongs to this user
const existingIdea = await prisma.idea.findUnique({
where: {
id,
userId: req.userId,
},
});
if (!existingIdea) {
res.status(404).json({
success: false,
message: "Idea not found or you don't have permission to delete it",
});
return;
}
await prisma.idea.delete({
where: {
id,
},
});
res.status(200).json({
success: true,
message: "Idea deleted successfully",
});
return;
} catch (error) {
console.error("Error deleting idea:", error);
res.status(500).json({
success: false,
message: "Failed to delete idea",
});
return;
}
});
// Search ideas by term (searches in title, content, and tags)
router.get("/searchIdeas", authMiddleware, async (req: any, res) => {
try {
const { term } = req.query;
if (!term || typeof term !== 'string') {
res.status(400).json({
success: false,
message: "Search term is required",
});
return;
}
const ideas = await prisma.idea.findMany({
where: {
userId: req.userId,
OR: [
{ title: { contains: term, mode: 'insensitive' } },
{ tags: { has: term } },
// Note: Searching in Slate content is complex and depends on your DB
// You might need a custom solution or full-text search for content
],
},
orderBy: {
updatedAt: "desc",
},
});
res.status(200).json({
success: true,
data: ideas,
});
return
} catch (error) {
console.error("Error searching ideas:", error);
res.status(500).json({
success: false,
message: "Failed to search ideas",
});
return
}
});
export default router;
// AIzaSyAGXspPFiKkmicShhkyuGnUvqxuaWbBtKE

View File

@ -0,0 +1,12 @@
import express from "express"
import AuthRouter from "./auth.js"
import IdeaRouter from "./ideas.js"
import EventRouter from "./events.js"
const router = express.Router()
router.use("/auth", AuthRouter)
router.use("/ideas", IdeaRouter )
router.use("/events", EventRouter)
export default router

View File

@ -0,0 +1,69 @@
import jwt, { JwtPayload } from "jsonwebtoken";
import JWT_SECRET from "./config.js";
interface CustomJwtPayload extends JwtPayload {
userId: string;
}
const authMiddleware = (req:any, res: any, next: any) => {
const authHeader = req.headers.authorization;
// Check if Authorization header exists
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.status(401).json({
success: false,
message: "No authentication token provided"
});
return;
}
const token = authHeader.split(" ")[1];
if (!token) {
res.status(401).json({
success: false,
message: "Invalid token format"
});
return;
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as CustomJwtPayload
if (decoded && decoded.userId) {
req.userId = decoded.userId;
next();
} else {
res.status(403).json({
success: false,
message: "Invalid token payload"
});
return;
}
} catch (error: any) {
if (error.name === "TokenExpiredError") {
res.status(401).json({
success: false,
message: "Token expired"
});
return;
} else if (error.name === "JsonWebTokenError") {
res.status(401).json({
success: false,
message: "Invalid token"
});
return;
} else {
res.status(500).json({
success: false,
message: "Authentication error"
});
return;
}
}
};
export default authMiddleware;

26
apps/backend/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "npx esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js",
"start": "node dist/index.js",
"dev": "npm run build && npm run start"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@google/genai": "^0.13.0",
"@repo/db": "*",
"@types/cors": "^2.8.18",
"@types/express": "^5.0.1",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.17",
"cors": "^2.8.5",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mime": "^4.0.7",
"zod": "^3.24.4"
}
}

15
apps/backend/src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import express from "express"
import mainRouter from "./../mainRouter.js"
import cors from "cors"
const app = express()
app.use(cors())
app.use(express.json())
const router = express.Router()
app.use("/v1", mainRouter)
app.listen(3000)

69
apps/backend/temp.js Normal file
View File

@ -0,0 +1,69 @@
let temp = {
"platforms": {
"instagram": {
"title": "3 Business Lessons I Wish I Knew Sooner!",
"description": "Here are 3 game-changing business lessons that could save you time, money, and headaches! What's the biggest lesson YOU'VE learned? Share in the comments! 👇\n#businessowner #entrepreneurship #smallbusiness #businesstips #leadership",
"hashtags": [
"businessowner",
"entrepreneurship",
"smallbusiness",
"businesstips",
"leadership"
],
"callToAction": "What's the biggest lesson YOU'VE learned?",
"bestPostingTimes": "Tuesdays-Thursdays, 9 AM - 12 PM",
"contentFormat": "Reel",
"additionalTips": "Use trending audio to increase reach.",
"contentDetails": {
"scenes": [
{
"description": "Opening shot of you in a business setting (office, meeting, etc.). Look directly at the camera with a confident expression.",
"text": "3 Business Lessons I Wish I Knew Sooner!",
"duration": 3
},
{
"description": "Show a quick montage of mistakes or challenges you faced early on. Use fast cuts and slightly comedic music in the background.",
"text": "Lesson 1: Don't be afraid to fail.",
"duration": 5
},
{
"description": "Transition to a scene where you are learning from a mistake. Show you reading a book, attending a workshop, or talking to a mentor.",
"text": "Find a mentor!",
"duration": 5
},
{
"description": "Show yourself successfully implementing a new strategy or overcoming a challenge. Use uplifting music.",
"text": "Lesson 2: Invest in your team",
"duration": 5
},
{
"description": "Quick cuts showcasing team collaboration, positive feedback, and successful project outcomes.",
"text": "A strong team makes all the difference.",
"duration": 5
},
{
"description": "Show a before-and-after scenario of your business (e.g., messy office vs. organized office, low sales vs. high sales).",
"text": "Lesson 3: Automate repetitive tasks",
"duration": 5
},
{
"description": "Show examples of automation tools you use (e.g., CRM, email marketing platform, social media scheduler).",
"text": "Use technology to your advantage!",
"duration": 5
},
{
"description": "End with a call to action, pointing to the comments section.",
"text": "What's the biggest lesson YOU'VE learned?",
"duration": 2
}
],
"audio": "Trending upbeat background music or a motivational speech excerpt.",
"transitions": "Use dynamic transitions like zooms, wipes, and quick cuts to maintain viewer engagement."
}
}
},
"generalTips": []
}
const paragraphs = temp.split(/\n\n+/)
console.log(paragraphs)

View File

@ -0,0 +1,8 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

36
apps/docs/.gitignore vendored
View File

@ -1,36 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,50 +0,0 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
.imgDark {
display: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
.imgLight {
display: none;
}
.imgDark {
display: unset;
}
}

View File

@ -1,31 +0,0 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}

View File

@ -1,188 +0,0 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-synthesis: none;
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 180px;
}
button.secondary {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
background: transparent;
border-color: var(--gray-alpha-200);
min-width: 180px;
}
.footer {
font-family: var(--font-geist-sans);
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}

View File

@ -1,102 +0,0 @@
import Image, { type ImageProps } from "next/image";
import { Button } from "@repo/ui/button";
import styles from "./page.module.css";
type Props = Omit<ImageProps, "src"> & {
srcLight: string;
srcDark: string;
};
const ThemeImage = (props: Props) => {
const { srcLight, srcDark, ...rest } = props;
return (
<>
<Image {...rest} src={srcLight} className="imgLight" />
<Image {...rest} src={srcDark} className="imgDark" />
</>
);
};
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<ThemeImage
className={styles.logo}
srcLight="turborepo-dark.svg"
srcDark="turborepo-light.svg"
alt="Turborepo logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>apps/docs/app/page.tsx</code>
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new/clone?demo-description=Learn+to+implement+a+monorepo+with+a+two+Next.js+sites+that+has+installed+three+local+packages.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4K8ZISWAzJ8X1504ca0zmC%2F0b21a1c6246add355e55816278ef54bc%2FBasic.png&demo-title=Monorepo+with+Turborepo&demo-url=https%3A%2F%2Fexamples-basic-web.vercel.sh%2F&from=templates&project-name=Monorepo+with+Turborepo&repository-name=monorepo-turborepo&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fturborepo%2Ftree%2Fmain%2Fexamples%2Fbasic&root-directory=apps%2Fdocs&skippable-integrations=1&teamSlug=vercel&utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://turborepo.com/docs?utm_source"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
<Button appName="docs" className={styles.secondary}>
Open alert
</Button>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com/templates?search=turborepo&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://turborepo.com?utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to turborepo.com
</a>
</footer>
</div>
);
}

View File

@ -1,4 +0,0 @@
import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config} */
export default nextJsConfig;

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View File

@ -1,28 +0,0 @@
{
"name": "docs",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3001",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@repo/ui": "*",
"next": "^15.3.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.26.0",
"typescript": "5.8.2"
}
}

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 13.5V6.5V5.41421C14.5 5.149 14.3946 4.89464 14.2071 4.70711L9.79289 0.292893C9.60536 0.105357 9.351 0 9.08579 0H8H3H1.5V1.5V13.5C1.5 14.8807 2.61929 16 4 16H12C13.3807 16 14.5 14.8807 14.5 13.5ZM13 13.5V6.5H9.5H8V5V1.5H3V13.5C3 14.0523 3.44772 14.5 4 14.5H12C12.5523 14.5 13 14.0523 13 13.5ZM9.5 5V2.12132L12.3787 5H9.5ZM5.13 5.00062H4.505V6.25062H5.13H6H6.625V5.00062H6H5.13ZM4.505 8H5.13H11H11.625V9.25H11H5.13H4.505V8ZM5.13 11H4.505V12.25H5.13H11H11.625V11H11H5.13Z" fill="#666666"/>
</svg>

Before

Width:  |  Height:  |  Size: 645 B

View File

@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_868_525)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z" fill="#666666"/>
</g>
<defs>
<clipPath id="clip0_868_525">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,19 +0,0 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6565V22.3773H91.0977V30.6565H106.16V58.1875H115.935V30.6565H130.998Z" fill="black"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2275V22.3773H162.768V41.2799C162.768 47.0155 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0155 144.315 41.2799V22.3773H134.539V42.2275C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="black"/>
<path d="M187.508 46.3173H197.234L204.914 58.1875H216.136L207.458 45.2699C212.346 43.5243 215.338 39.634 215.338 34.3473C215.338 26.6665 209.603 22.3773 200.874 22.3773H177.732V58.1875H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.053 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="black"/>
<path d="M219.887 58.1875H245.472C253.452 58.1875 258.041 54.397 258.041 48.0629C258.041 43.8235 255.348 40.9308 252.156 39.634C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1875ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0205 248.615 46.9657C248.615 48.9108 247.168 50.2075 244.525 50.2075H229.263V43.7238Z" fill="black"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.786 281.942 58.786C294.461 58.786 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2575C288.525 30.2575 293.463 34.1478 293.463 40.2824C293.463 46.417 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.417 270.421 40.2824C270.421 34.1478 275.359 30.2575 281.942 30.2575Z" fill="black"/>
<path d="M317.526 46.3173H327.251L334.932 58.1875H346.154L337.476 45.2699C342.364 43.5243 345.356 39.634 345.356 34.3473C345.356 26.6665 339.62 22.3773 330.892 22.3773H307.75V58.1875H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.053 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="black"/>
<path d="M349.904 22.3773V58.1875H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6565H384.717V22.3773H349.904Z" fill="black"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1875H399.204V46.7662ZM399.204 38.6365V30.5568H411.673C415.164 30.5568 417.059 32.053 417.059 34.5967C417.059 37.0904 415.164 38.6365 411.673 38.6365H399.204Z" fill="black"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.786 450.948 58.786C463.467 58.786 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2575C457.532 30.2575 462.469 34.1478 462.469 40.2824C462.469 46.417 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.417 439.427 40.2824C439.427 34.1478 444.365 30.2575 450.948 30.2575Z" fill="black"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_278)"/>
<defs>
<linearGradient id="paint0_linear_2028_278" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,19 +0,0 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6566V22.3773H91.0977V30.6566H106.16V58.1876H115.935V30.6566H130.998Z" fill="white"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2276V22.3773H162.768V41.2799C162.768 47.0156 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0156 144.315 41.2799V22.3773H134.539V42.2276C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="white"/>
<path d="M187.508 46.3173H197.234L204.914 58.1876H216.136L207.458 45.2699C212.346 43.5243 215.338 39.6341 215.338 34.3473C215.338 26.6666 209.603 22.3773 200.874 22.3773H177.732V58.1876H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.0531 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="white"/>
<path d="M219.887 58.1876H245.472C253.452 58.1876 258.041 54.3971 258.041 48.0629C258.041 43.8236 255.348 40.9308 252.156 39.6341C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1876ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0206 248.615 46.9657C248.615 48.9108 247.168 50.2076 244.525 50.2076H229.263V43.7238Z" fill="white"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.7861 281.942 58.7861C294.461 58.7861 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2576C288.525 30.2576 293.463 34.1478 293.463 40.2824C293.463 46.4171 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.4171 270.421 40.2824C270.421 34.1478 275.359 30.2576 281.942 30.2576Z" fill="white"/>
<path d="M317.526 46.3173H327.251L334.932 58.1876H346.154L337.476 45.2699C342.364 43.5243 345.356 39.6341 345.356 34.3473C345.356 26.6666 339.62 22.3773 330.892 22.3773H307.75V58.1876H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.0531 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="white"/>
<path d="M349.904 22.3773V58.1876H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6566H384.717V22.3773H349.904Z" fill="white"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1876H399.204V46.7662ZM399.204 38.6366V30.5568H411.673C415.164 30.5568 417.059 32.0531 417.059 34.5967C417.059 37.0904 415.164 38.6366 411.673 38.6366H399.204Z" fill="white"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.7861 450.948 58.7861C463.467 58.7861 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2576C457.532 30.2576 462.469 34.1478 462.469 40.2824C462.469 46.4171 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.4171 439.427 40.2824C439.427 34.1478 444.365 30.2576 450.948 30.2576Z" fill="white"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_477)"/>
<defs>
<linearGradient id="paint0_linear_2028_477" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,10 +0,0 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_977_547)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3L18.5 17H2.5L10.5 3Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_977_547">
<rect width="16" height="16" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 367 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM3.75 5.5C4.16421 5.5 4.5 5.16421 4.5 4.75C4.5 4.33579 4.16421 4 3.75 4C3.33579 4 3 4.33579 3 4.75C3 5.16421 3.33579 5.5 3.75 5.5ZM7 4.75C7 5.16421 6.66421 5.5 6.25 5.5C5.83579 5.5 5.5 5.16421 5.5 4.75C5.5 4.33579 5.83579 4 6.25 4C6.66421 4 7 4.33579 7 4.75ZM8.75 5.5C9.16421 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.16421 4 8.75 4C8.33579 4 8 4.33579 8 4.75C8 5.16421 8.33579 5.5 8.75 5.5Z" fill="#666666"/>
</svg>

Before

Width:  |  Height:  |  Size: 750 B

View File

@ -1,20 +0,0 @@
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
"next.config.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

24
apps/project1/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

54
apps/project1/README.md Normal file
View File

@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
apps/project1/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

8031
apps/project1/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,67 @@
{
"name": "project1",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@syncfusion/ej2-react-buttons": "^29.1.34",
"@syncfusion/ej2-react-dropdowns": "^29.1.41",
"@syncfusion/ej2-react-richtexteditor": "^29.1.40",
"@tailwindcss/vite": "^4.1.5",
"@tiptap/extension-highlight": "^2.12.0",
"@tiptap/extension-image": "^2.12.0",
"@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/extension-subscript": "^2.12.0",
"@tiptap/extension-superscript": "^2.12.0",
"@tiptap/extension-task-item": "^2.12.0",
"@tiptap/extension-task-list": "^2.12.0",
"@tiptap/extension-text-align": "^2.12.0",
"@tiptap/extension-typography": "^2.12.0",
"@tiptap/extension-underline": "^2.12.0",
"@tiptap/extension-youtube": "^2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"axios": "^1.9.0",
"framer-motion": "^12.9.4",
"lucide-react": "^0.507.0",
"moment": "^2.30.1",
"motion": "^12.9.4",
"novel": "^1.0.2",
"react": "^19.0.0",
"react-big-calendar": "^1.18.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.5.3",
"remark-gfm": "^4.0.1",
"slate": "^0.114.0",
"slate-history": "^0.113.1",
"slate-react": "^0.114.2",
"smart-webcomponents-react": "^22.0.0",
"tailwindcss": "^4.1.5"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@types/react": "^19.0.10",
"@types/react-big-calendar": "^1.16.1",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.26.1",
"vite": "^6.3.1"
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 512 512" id="icons" xmlns="http://www.w3.org/2000/svg"><path d="M208,512a24.84,24.84,0,0,1-23.34-16l-39.84-103.6a16.06,16.06,0,0,0-9.19-9.19L32,343.34a25,25,0,0,1,0-46.68l103.6-39.84a16.06,16.06,0,0,0,9.19-9.19L184.66,144a25,25,0,0,1,46.68,0l39.84,103.6a16.06,16.06,0,0,0,9.19,9.19l103,39.63A25.49,25.49,0,0,1,400,320.52a24.82,24.82,0,0,1-16,22.82l-103.6,39.84a16.06,16.06,0,0,0-9.19,9.19L231.34,496A24.84,24.84,0,0,1,208,512Zm66.85-254.84h0Z"/><path d="M88,176a14.67,14.67,0,0,1-13.69-9.4L57.45,122.76a7.28,7.28,0,0,0-4.21-4.21L9.4,101.69a14.67,14.67,0,0,1,0-27.38L53.24,57.45a7.31,7.31,0,0,0,4.21-4.21L74.16,9.79A15,15,0,0,1,86.23.11,14.67,14.67,0,0,1,101.69,9.4l16.86,43.84a7.31,7.31,0,0,0,4.21,4.21L166.6,74.31a14.67,14.67,0,0,1,0,27.38l-43.84,16.86a7.28,7.28,0,0,0-4.21,4.21L101.69,166.6A14.67,14.67,0,0,1,88,176Z"/><path d="M400,256a16,16,0,0,1-14.93-10.26l-22.84-59.37a8,8,0,0,0-4.6-4.6l-59.37-22.84a16,16,0,0,1,0-29.86l59.37-22.84a8,8,0,0,0,4.6-4.6L384.9,42.68a16.45,16.45,0,0,1,13.17-10.57,16,16,0,0,1,16.86,10.15l22.84,59.37a8,8,0,0,0,4.6,4.6l59.37,22.84a16,16,0,0,1,0,29.86l-59.37,22.84a8,8,0,0,0-4.6,4.6l-22.84,59.37A16,16,0,0,1,400,256Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
apps/project1/src/App.css Normal file
View File

@ -0,0 +1,42 @@
/* #root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
} */

40
apps/project1/src/App.tsx Normal file
View File

@ -0,0 +1,40 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import './App.css'
import { Landing } from './pages/Landing'
import { AuthProvider } from './context/AuthContext'
import Login from './pages/auth/Login'
import Layout from './layout/Layout'
// import { ThemeProvider } from './context/ThemeContext'
import Register from './pages/auth/Register'
import { Ideas } from './pages/ideas/Ideas'
import { IdeasProvider } from './context/IdeasContext'
import Calendar from './pages/schedule/Scheduler'
import ProtectedRoute from './layout/ProtectedRoute'
import { EventsProvider } from './context/EventsContext'
function App() {
return (
<BrowserRouter>
{/* <ThemeProvider> */}
<AuthProvider>
<IdeasProvider>
<EventsProvider>
<Layout>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/ideas" element={<ProtectedRoute><Ideas /></ProtectedRoute>} />
<Route path="/schedule" element={<ProtectedRoute><Calendar /></ProtectedRoute>} />
</Routes>
</Layout>
</EventsProvider>
</IdeasProvider>
</AuthProvider>
{/* </ThemeProvider> */}
</BrowserRouter>
)
}
export default App

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,54 @@
html,
body,
body > div { /* the react root */
margin: 0;
padding: 0;
/* height: 100%; */
}
h2 {
margin: 0;
font-size: 16px;
}
ul {
margin: 0;
padding: 0 0 0 1.5em;
}
li {
margin: 1.5em 0;
padding: 0;
}
b { /* used for event dates/times */
margin-right: 3px;
}
.demo-app {
display: flex;
/* min-height: 100%; */
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
.demo-app-sidebar {
width: 300px;
line-height: 1.5;
background: #eaf9ff;
border-right: 1px solid #d3e2e8;
}
.demo-app-sidebar-section {
padding: 2em;
}
.demo-app-main {
flex-grow: 1;
padding: 3em;
}
.fc { /* the calendar root */
max-width: 1100px;
margin: 0 auto;
}

View File

@ -0,0 +1,408 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import Image from '@tiptap/extension-image';
import Link from '@tiptap/extension-link';
import Youtube from '@tiptap/extension-youtube';
import { Slash, Bold, Italic, List, ListOrdered, Heading1, Heading2, Code, Quote, Check, Grip, Plus, Image as ImageIcon, Video, Link as LinkIcon, X } from 'lucide-react';
const NotionLikeEditor = () => {
const [showBubbleMenu, setShowBubbleMenu] = useState(false);
const [showBlockMenu, setShowBlockMenu] = useState(false);
const [blockMenuPosition, setBlockMenuPosition] = useState({ top: 0, left: 0 });
const [showLinkMenu, setShowLinkMenu] = useState(false);
const [linkUrl, setLinkUrl] = useState('');
const [showImageMenu, setShowImageMenu] = useState(false);
const [imageUrl, setImageUrl] = useState('');
const [showVideoMenu, setShowVideoMenu] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const linkInputRef = useRef<HTMLInputElement | null>(null);
const editor = useEditor({
extensions: [
StarterKit,
Placeholder.configure({
placeholder: 'Type "/" for commands...',
}),
Image.configure({
inline: false,
allowBase64: true,
}),
Link.configure({
openOnClick: true,
linkOnPaste: true,
}),
Youtube.configure({
controls: true,
nocookie: true,
}),
],
content: '<p>Welcome to the Notion-like editor! Try typing "/" to access the block menu.</p>',
onSelectionUpdate: ({ editor }) => {
setShowBubbleMenu(!editor.state.selection.empty);
},
onUpdate: ({ editor }) => {
// Check for '/' at the start of a paragraph to show block menu
const selection = editor.state.selection;
const text = editor.getText();
if (text.trim() === '/') {
const node = editor.view.domAtPos(selection.from);
// Access the DOM node correctly and check if it exists
if (node && node.node && node.node instanceof HTMLElement) {
const rect = node.node.getBoundingClientRect();
setBlockMenuPosition({
top: rect.bottom,
left: rect.left,
});
setShowBlockMenu(true);
}
} else {
setShowBlockMenu(false);
}
}
});
const blockTypes = [
{ icon: <Heading1 size={18} />, title: 'Heading 1', action: () => toggleBlock('h1') },
{ icon: <Heading2 size={18} />, title: 'Heading 2', action: () => toggleBlock('h2') },
{ icon: <List size={18} />, title: 'Bullet List', action: () => toggleBlock('bulletList') },
{ icon: <ListOrdered size={18} />, title: 'Ordered List', action: () => toggleBlock('orderedList') },
{ icon: <Code size={18} />, title: 'Code Block', action: () => toggleBlock('codeBlock') },
{ icon: <Quote size={18} />, title: 'Blockquote', action: () => toggleBlock('blockquote') },
{ icon: <Check size={18} />, title: 'Task List', action: () => toggleBlock('taskList') },
{ icon: <ImageIcon size={18} />, title: 'Image', action: () => toggleBlock('image') },
{ icon: <Video size={18} />, title: 'Video', action: () => toggleBlock('video') },
{ icon: <LinkIcon size={18} />, title: 'Link', action: () => toggleBlock('link') },
];
const toggleBlock = (blockType: string) => {
if (!editor) return;
editor.commands.deleteRange({
from: editor.state.selection.from - 1,
to: editor.state.selection.from
});
switch (blockType) {
case 'h1':
editor.chain().focus().toggleHeading({ level: 1 }).run();
break;
case 'h2':
editor.chain().focus().toggleHeading({ level: 2 }).run();
break;
case 'bulletList':
editor.chain().focus().toggleBulletList().run();
break;
case 'orderedList':
editor.chain().focus().toggleOrderedList().run();
break;
case 'codeBlock':
editor.chain().focus().toggleCodeBlock().run();
break;
case 'blockquote':
editor.chain().focus().toggleBlockquote().run();
break;
case 'taskList':
// Simple paragraph with checkbox for demo purposes
editor.chain().focus().insertContent('☐ Task item').run();
break;
case 'image':
setShowImageMenu(true);
break;
case 'video':
setShowVideoMenu(true);
break;
case 'link':
setShowLinkMenu(true);
setTimeout(() => {
if (linkInputRef.current) {
linkInputRef.current.focus();
}
}, 0);
break;
default:
break;
}
setShowBlockMenu(false);
};
const addImage = () => {
if (imageUrl && editor) {
editor.chain().focus().setImage({ src: imageUrl }).run();
setImageUrl('');
setShowImageMenu(false);
}
};
const addVideo = () => {
if (videoUrl && editor) {
editor.chain().focus().setYoutubeVideo({ src: videoUrl }).run();
setVideoUrl('');
setShowVideoMenu(false);
}
};
const addLink = () => {
if (linkUrl && editor) {
editor.chain().focus().setLink({ href: linkUrl }).run();
setLinkUrl('');
setShowLinkMenu(false);
}
};
// Handle keyboard navigation in block menu
const handleBlockMenuKeyDown = useCallback((event: { key: string; }) => {
if (!showBlockMenu) return;
if (event.key === 'Escape') {
setShowBlockMenu(false);
}
}, [showBlockMenu]);
useEffect(() => {
window.addEventListener('keydown', handleBlockMenuKeyDown);
return () => {
window.removeEventListener('keydown', handleBlockMenuKeyDown);
};
}, [handleBlockMenuKeyDown]);
if (!editor) {
return null;
}
return (
<div className="w-full max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-6 min-h-96 border border-gray-200">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-800">Notion-like Editor</h1>
</div>
<div className="flex items-center space-x-2">
<button className="p-2 text-gray-500 hover:text-gray-700">
<Plus size={20} />
</button>
</div>
</div>
<div className="relative">
{editor && (
<BubbleMenu
editor={editor}
tippyOptions={{ duration: 100 }}
shouldShow={({ editor }) => !editor.state.selection.empty}
>
<div className="flex bg-white shadow-md rounded-md border border-gray-200">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={`p-2 hover:bg-gray-100 ${editor.isActive('bold') ? 'text-blue-500' : 'text-gray-600'}`}
>
<Bold size={16} />
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`p-2 hover:bg-gray-100 ${editor.isActive('italic') ? 'text-blue-500' : 'text-gray-600'}`}
>
<Italic size={16} />
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={`p-2 hover:bg-gray-100 ${editor.isActive('code') ? 'text-blue-500' : 'text-gray-600'}`}
>
<Code size={16} />
</button>
<button
onClick={() => setShowLinkMenu(true)}
className={`p-2 hover:bg-gray-100 ${editor.isActive('link') ? 'text-blue-500' : 'text-gray-600'}`}
>
<LinkIcon size={16} />
</button>
</div>
</BubbleMenu>
)}
{showLinkMenu && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 w-96">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Add Link</h3>
<button onClick={() => setShowLinkMenu(false)} className="text-gray-500 hover:text-gray-700">
<X size={18} />
</button>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">URL</label>
<input
type="text"
ref={linkInputRef}
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
placeholder="https://example.com"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === 'Enter') {
addLink();
}
}}
/>
</div>
<div className="flex justify-end space-x-2">
<button
onClick={() => setShowLinkMenu(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={addLink}
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600"
>
Add Link
</button>
</div>
</div>
</div>
)}
{showImageMenu && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 w-96">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Add Image</h3>
<button onClick={() => setShowImageMenu(false)} className="text-gray-500 hover:text-gray-700">
<X size={18} />
</button>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Image URL</label>
<input
type="text"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === 'Enter') {
addImage();
}
}}
/>
</div>
<div className="flex justify-end space-x-2">
<button
onClick={() => setShowImageMenu(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={addImage}
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600"
>
Add Image
</button>
</div>
</div>
</div>
)}
{showVideoMenu && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 w-96">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Add YouTube Video</h3>
<button onClick={() => setShowVideoMenu(false)} className="text-gray-500 hover:text-gray-700">
<X size={18} />
</button>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">YouTube URL</label>
<input
type="text"
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === 'Enter') {
addVideo();
}
}}
/>
</div>
<div className="flex justify-end space-x-2">
<button
onClick={() => setShowVideoMenu(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={addVideo}
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600"
>
Add Video
</button>
</div>
</div>
</div>
)}
{showBlockMenu && (
<div
className="absolute z-10 bg-white shadow-xl rounded-md border border-gray-200 min-w-56"
style={{
top: blockMenuPosition.top + 'px',
left: blockMenuPosition.left + 'px'
}}
>
<div className="p-2 border-b border-gray-200">
<div className="flex items-center gap-2 text-sm text-gray-500">
<Slash size={14} />
<span>Insert block</span>
</div>
</div>
<div className="py-1">
{blockTypes.map((block, index) => (
<div
key={index}
className="flex items-center px-3 py-2 hover:bg-gray-100 cursor-pointer"
onClick={block.action}
>
<div className="mr-2 text-gray-600">{block.icon}</div>
<div className="text-sm">{block.title}</div>
</div>
))}
</div>
</div>
)}
<div className="notion-editor prose max-w-none">
{!editor.isEmpty && (
<div className="flex group">
<div className="pt-1 pr-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Grip size={16} className="text-gray-400" />
</div>
<div className="flex-grow">
<EditorContent editor={editor} />
</div>
</div>
)}
{editor.isEmpty && (
<EditorContent editor={editor} />
)}
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200 text-sm text-gray-500">
Type <span className="px-1 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">/</span> for commands
</div>
</div>
</div>
);
};
export default NotionLikeEditor;

View File

@ -0,0 +1,64 @@
import React from 'react';
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
isLoading = false,
fullWidth = false,
leftIcon,
rightIcon,
className = '',
disabled,
...rest
}) => {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none';
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-(--primary-600) text-white hover:bg-primary-700 active:bg-primary-800',
secondary: 'bg-secondary-600 text-white hover:bg-secondary-700 active:bg-secondary-800',
outline: 'border border-gray-300 dark:border-gray-600 bg-transparent hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-gray-100',
ghost: 'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-900 dark:text-gray-100',
danger: 'bg-error-600 text-white hover:bg-error-700 active:bg-error-800',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'text-xs px-3 py-1.5 h-8',
md: 'text-sm px-4 py-2 h-10',
lg: 'text-base px-5 py-2.5 h-12',
};
const widthStyle = fullWidth ? 'w-full' : '';
return (
<button
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${widthStyle} ${className}`}
disabled={isLoading || disabled}
{...rest}
>
{isLoading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{!isLoading && leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{!isLoading && rightIcon && <span className="ml-2">{rightIcon}</span>}
</button>
);
};
export default Button;

View File

@ -0,0 +1,54 @@
import React from 'react';
interface CardProps {
children: React.ReactNode;
className?: string;
}
export const Card: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`bg-white rounded-lg border border-gray-400 shadow-sm ${className}`}>
{children}
</div>
);
};
export const CardHeader: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 border-b text-slate-600 border-gray-400 ${className}`}>
{children}
</div>
);
};
export const CardTitle: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<h3 className={`text-lg font-semibold text-slate-900 ${className}`}>
{children}
</h3>
);
};
export const CardDescription: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<p className={`text-sm text-slate-600 ${className}`}>
{children}
</p>
);
};
export const CardContent: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 ${className}`}>
{children}
</div>
);
};
export const CardFooter: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 border-t border-gray-400 ${className}`}>
{children}
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
import React, { useState, useEffect } from 'react';
import { CalendarEvent } from '../../context/EventsContext';
import { DateSelectArg, EventClickArg } from '@fullcalendar/core';
interface EventModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (event: Partial<CalendarEvent>) => void;
onDelete?: () => void;
selectedEvent?: CalendarEvent | null;
selectedDates?: DateSelectArg | null;
}
const EventModal: React.FC<EventModalProps> = ({
isOpen,
onClose,
onSave,
onDelete,
selectedEvent,
selectedDates,
}) => {
const [eventData, setEventData] = useState<Partial<CalendarEvent>>({
title: '',
description: '',
allDay: false,
color: '#3788d8', // Default color
});
// Update form when selectedEvent or selectedDates changes
useEffect(() => {
if (selectedEvent) {
// Editing an existing event
setEventData({
id: selectedEvent.id,
title: selectedEvent.title,
start: selectedEvent.start,
end: selectedEvent.end,
allDay: selectedEvent.allDay,
description: selectedEvent.description || '',
color: selectedEvent.color || '#3788d8',
});
} else if (selectedDates) {
// Creating a new event
setEventData({
title: '',
start: selectedDates.startStr,
end: selectedDates.endStr,
allDay: selectedDates.allDay,
description: '',
color: '#3788d8',
});
}
}, [selectedEvent, selectedDates]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checkbox = e.target as HTMLInputElement;
setEventData(prev => ({ ...prev, [name]: checkbox.checked }));
} else {
setEventData(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(eventData);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">
{selectedEvent ? 'Edit Event' : 'Create Event'}
</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"
>
&times;
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Title</label>
<input
type="text"
name="title"
value={eventData.title || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
required
/>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-1">Start</label>
<input
type="datetime-local"
name="start"
value={eventData.start instanceof Date
? eventData.start.toISOString().slice(0, 16)
: typeof eventData.start === 'string'
? eventData.start.slice(0, 16)
: ''}
onChange={handleChange}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">End</label>
<input
type="datetime-local"
name="end"
value={eventData.end instanceof Date
? eventData.end.toISOString().slice(0, 16)
: typeof eventData.end === 'string'
? eventData.end.slice(0, 16)
: ''}
onChange={handleChange}
className="w-full p-2 border rounded"
/>
</div>
</div>
<div className="mb-4">
<label className="flex items-center">
<input
type="checkbox"
name="allDay"
checked={eventData.allDay || false}
onChange={handleChange}
className="mr-2"
/>
<span className="text-sm font-medium">All Day</span>
</label>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
name="description"
value={eventData.description || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
rows={3}
/>
</div>
<div className="mb-6">
<label className="block text-sm font-medium mb-1">Color</label>
<input
type="color"
name="color"
value={eventData.color || '#3788d8'}
onChange={handleChange}
className="w-full p-1 border rounded h-10"
/>
</div>
<div className="flex justify-between">
{selectedEvent && onDelete && (
<button
type="button"
onClick={onDelete}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Delete
</button>
)}
<div className="ml-auto space-x-2">
<button
type="button"
onClick={onClose}
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Save
</button>
</div>
</div>
</form>
</div>
</div>
);
};
export default EventModal;

View File

@ -0,0 +1,40 @@
import { Lightbulb, Pen, BarChart3, BookOpen } from 'lucide-react';
export const Features = () => {
return <div className="flex flex-col gap-y-12 mt-10 mb-10">
<div className="flex justify-center text-3xl font-bold text-slate-700">
Everything you need for your creative journey
</div>
<div className="grid grid-cols-4 mx-32 place-items-center h-44 gap-x-8">
<div className="bg-slate-700 rounded-lg h-full w-full relative text-white">
<div className="bg-blue-500 rounded-md relative p-2 w-fit left-6 -top-5 z-2">
<Lightbulb stroke='white' />
</div>
<div className='px-3 text-lg font-medium'>Idea Management</div>
<div className='px-3 my-2'>Easily create, organize, and manage all your creative ideas in one place with intuitive tools.</div>
</div>
<div className="bg-slate-700 rounded-lg h-full w-full relative text-white">
<div className="bg-purple-500 rounded-md relative p-2 w-fit left-6 -top-5 z-2">
<Pen stroke='white' />
</div>
<div className='px-3 text-lg font-medium'>Idea Management</div>
<div className='px-3 my-2'>Easily create, organize, and manage all your creative ideas in one place with intuitive tools.</div>
</div>
<div className="bg-slate-700 rounded-lg h-full w-full relative text-white">
<div className="bg-green-500 rounded-md relative p-2 w-fit left-6 -top-5 z-2">
<BarChart3 stroke='white' />
</div>
<div className='px-3 text-lg font-medium'>Idea Management</div>
<div className='px-3 my-2'>Easily create, organize, and manage all your creative ideas in one place with intuitive tools.</div>
</div>
<div className="bg-slate-700 rounded-lg h-full w-full relative text-white">
<div className="bg-yellow-500 rounded-md relative p-2 w-fit left-6 -top-5 z-2">
<BookOpen stroke='white' />
</div>
<div className='px-3 text-lg font-medium'>Idea Management</div>
<div className='px-3 my-2'>Easily create, organize, and manage all your creative ideas in one place with intuitive tools.</div>
</div>
</div>
</div>
}

View File

@ -0,0 +1,17 @@
export const Hero1 = () => {
return <div className="mt-30 ml-5 font-bold h-[300px] max-h-3/4">
<div className="flex flex-col">
<div className="flex flex-col">
<div className="text-6xl text-slate-700">Welcome to CreatorHub
</div>
<div className="text-4xl text-slate-500 mt-8">Your creative ideas brought to life</div>
<div className="text-lg text-slate-700 font-normal leading-5 mt-5">
Easily capture, organize, and develop your creative ideas from concept to completion. Track your progress, store content, and bring your best ideas to life.</div>
</div>
<div className="pl-5 pt-5">
{/* <img src="/vite.svg" height={100} width={100} alt="img"></img> */}
</div>
</div>
</div>
}

View File

@ -0,0 +1,176 @@
// import { useState } from "react";
// import { useIdeas, Idea } from "../../context/IdeasContext";
// import CreateIdea from "./CreateIdea";
// type IdeaCardProps = {
// idea: Idea;
// };
// const IdeaCard = ({ idea }: IdeaCardProps) => {
// const [showEditForm, setShowEditForm] = useState(false);
// const { getStatusColor, getTagColor } = useIdeas();
// return (
// <div className="grid grid-cols-4 mx-2 mb-4">
// <div className="flex flex-col shadow-md rounded bg-gray-200 p-4">
// <div className="text-black text-3xl font-semibold mb-4">
// <button onClick={() => setShowEditForm(true)}>
// {idea.title}
// </button>
// </div>
// <div className="flex flex-col gap-y-4">
// <div className="flex items-center">
// <div className="mr-2">Status:</div>
// <span
// className={`${getStatusColor(idea.status)} text-white text-xs px-2 py-1 rounded-md`}
// >
// {idea.status}
// </span>
// </div>
// {idea.tags.length > 0 && (
// <div className="flex">
// <div className="">Tags:</div>
// <div className="flex flex-wrap ml-5 gap-1">
// {idea.tags.map((tag, index) => (
// <span
// key={index}
// className={`${getTagColor(tag)} text-white text-sm px-2 rounded-full`}
// >
// {tag}
// </span>
// ))}
// </div>
// </div>
// )}
// </div>
// </div>
// {showEditForm && (
// <CreateIdea
// onClose={() => setShowEditForm(false)}
// ideaToEdit={idea}
// />
// )}
// </div>
// );
// };
// export default IdeaCard;
import { useState } from "react";
import { useIdeas, Idea } from "../../context/IdeasContext";
import CreateIdea from "./CreateIdea";
import { Node } from "slate";
type IdeaCardProps = {
idea: Idea;
onEdit: (idea: Idea) => void
};
// Helper function to get plain text from Slate nodes
const getPlainText = (nodes: any[]): string => {
return nodes.map(n => Node.string(n)).join('\n');
};
// Helper to detect if the content has images
const hasImages = (nodes: any[]): boolean => {
return nodes.some(node =>
(node.type === 'image') ||
(node.children && hasImages(node.children))
);
};
// Helper to get first image URL if any
const getFirstImageUrl = (nodes: any[]): string | null => {
for (const node of nodes) {
if (node.type === 'image' && node.url) {
return node.url;
}
if (node.children) {
const childImageUrl = getFirstImageUrl(node.children);
if (childImageUrl) return childImageUrl;
}
}
return null;
};
const IdeaCard = ({ idea, onEdit }: IdeaCardProps) => {
const { getStatusColor, getTagColor } = useIdeas();
const [isDeleting, setIsDeleting] = useState(false);
const { deleteIdea } = useIdeas()
const onDelete = async (ideaId: string) => {
if (window.confirm("Are you sure you want to delete this idea?")) {
setIsDeleting(true);
try {
await deleteIdea(ideaId);
} catch (error) {
console.error("Failed to delete idea:", error);
} finally {
setIsDeleting(false);
}
}
};
return (
<div className="mx-2 mb-4">
<div className="flex flex-col shadow-md rounded bg-gray-200 p-4 h-full">
<div className="text-black text-2xl font-semibold mb-4">
{idea.title}
{/* <button
onClick={() => onEdit(idea)}
className="text-left hover:text-blue-700 transition-colors"
>
{idea.title}
</button> */}
</div>
<div className="flex mb-4 gap-4">
<div>Status: </div>
<span
className={`${getStatusColor(idea.status)} text-white text-xs px-2 py-1 rounded-md mr-2`}
>
{idea.status}
</span>
</div>
<div className="flex mb-4 gap-x-1">
<div>Tags: </div>
{idea.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{idea.tags.map((tag, index) => (
<span
key={index}
className={`${getTagColor(tag)} text-white text-xs px-2 py-1 rounded-full`}
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex justify-end mt-auto gap-x-2">
<button
onClick={() => onEdit(idea)}
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
>
Edit
</button>
<button
onClick={() => onDelete(idea.id)}
disabled={isDeleting}
className={`text-red-600 hover:text-red-800 text-sm font-medium ${isDeleting ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
);
};
export default IdeaCard;

View File

@ -0,0 +1,77 @@
import React, { forwardRef } from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
label,
error,
helperText,
fullWidth = false,
leftIcon,
rightIcon,
className = '',
disabled,
...rest
},
ref
) => {
const baseInputStyles =
'flex h-10 rounded-md border bg-white px-3 py-2 text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring- focus-visible:ring-primary-500 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700';
const errorInputStyles = error
? 'border-error-500 focus-visible:ring-error-500'
: 'border-gray-300 dark:border-gray-600';
const widthStyle = fullWidth ? 'w-full' : '';
return (
<div className={`${widthStyle} space-y-1 ${className}`}>
{label && (
<label
className="text-sm font-medium leading-none text-slate-700"
htmlFor={rest.id}
>
{label}
</label>
)}
<div className="relative mt-2">
{leftIcon && (
<div className="absolute left-2 top-0 flex h-10 items-center text-gray-400">
{leftIcon}
</div>
)}
<input
ref={ref}
className={`${baseInputStyles} ${errorInputStyles} ${
leftIcon ? 'pl-9' : ''
} ${rightIcon ? 'pr-9' : ''} ${widthStyle}`}
disabled={disabled}
{...rest}
/>
{rightIcon && (
<div className="absolute right-2 top-0 flex h-10 items-center text-gray-400">
{rightIcon}
</div>
)}
</div>
{helperText && !error && (
<p className="text-xs text-gray-500 dark:text-gray-400">{helperText}</p>
)}
{error && <p className="text-xs text-error-600">{error}</p>}
</div>
);
}
);
Input.displayName = 'Input';
export default Input;

View File

@ -0,0 +1,340 @@
import React, { useState, useEffect } from 'react';
import RichTextEditor, { initialValue } from './RichTextEditor';
import { Descendant } from 'slate';
// Define the structure of the AI response based on the system prompt
interface PlatformContent {
title: string;
description: string;
hashtags: string[];
callToAction: string;
bestPostingTimes: string;
contentFormat: string;
additionalTips?: string;
contentDetails?: {
scenes?: {
description: string;
text: string;
duration: number;
}[];
audio?: string;
transitions?: string;
};
[key: string]: any; // For any additional platform-specific fields
}
export interface AIResponse {
platforms: {
instagram?: PlatformContent;
facebook?: PlatformContent;
twitter?: PlatformContent;
linkedin?: PlatformContent;
youtube?: PlatformContent;
[key: string]: PlatformContent | undefined;
};
generalTips: string[];
}
// Helper function to convert platform content to Slate nodes
const platformContentToSlateNodes = (platformContent: PlatformContent): Descendant[] => {
const nodes: Descendant[] = [];
// Add title
if (platformContent.title) {
nodes.push({
type: 'heading-one',
children: [{text: 'Title: ', bold: true}, { text: platformContent.title }]
});
}
// Add description
if (platformContent.description) {
nodes.push({
type: 'paragraph',
children: [{text: 'Description: ', bold: true}, { text: platformContent.description }]
});
}
// Add hashtags
if (platformContent.hashtags && platformContent.hashtags.length > 0) {
nodes.push({
type: 'heading-two',
children: [{text: 'Hashtags: ', bold: true}]
});
nodes.push({
type: 'paragraph',
children: [{ text: '#' + platformContent.hashtags.join(' #') }]
});
}
// Add call to action
if (platformContent.callToAction) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Call to Action', bold: true }, { text: platformContent.callToAction }]
});
// nodes.push({
// type: 'paragraph',
// children: [{ text: platformContent.callToAction }]
// });
}
// Add best posting times
if (platformContent.bestPostingTimes) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Best Posting Times: ', bold:true }]
});
nodes.push({
type: 'paragraph',
children: [{ text: platformContent.bestPostingTimes }]
});
}
// Add content format
if (platformContent.contentFormat) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Content Format: ', bold:true }, { text: platformContent.contentFormat }]
});
// nodes.push({
// type: 'paragraph',
// children: [{ text: platformContent.contentFormat }]
// });
}
// Add content details for visual content (like Instagram Reels)
if (platformContent.contentDetails) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Content Details: ' }]
});
// Add scenes if available
if (platformContent.contentDetails.scenes && platformContent.contentDetails.scenes.length > 0) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Scenes' }]
});
platformContent.contentDetails.scenes.forEach((scene, index) => {
nodes.push({
type: 'paragraph',
children: [{ text: `Scene ${index + 1}: `, bold: true }, { text: scene.description }]
});
nodes.push({
type: 'paragraph',
children: [{ text: `Text overlay: `, italic: true }, { text: scene.text }]
});
nodes.push({
type: 'paragraph',
children: [{ text: `Duration: ${scene.duration}s` }]
});
});
}
// Add audio
if (platformContent.contentDetails.audio) {
nodes.push({
type: 'paragraph',
children: [{ text: `Audio: `, bold: true }, { text: platformContent.contentDetails.audio }]
});
}
// Add transitions
if (platformContent.contentDetails.transitions) {
nodes.push({
type: 'paragraph',
children: [{ text: `Transitions: `, bold: true }, { text: platformContent.contentDetails.transitions }]
});
}
}
// Add additional tips
if (platformContent.additionalTips) {
nodes.push({
type: 'heading-two',
children: [{ text: 'Additional Tips' }]
});
nodes.push({
type: 'paragraph',
children: [{ text: platformContent.additionalTips }]
});
}
return nodes;
};
// Component for platform-specific content with dropdown selector
const PlatformSpecificContent: React.FC<{
aiResponse: string;
onChange: (value: Descendant[], fullAIResponse: AIResponse | null) => void;
existingAIResponse?: AIResponse | null;
}> = ({ aiResponse, onChange, existingAIResponse }) => {
// console.log("aiResponse: ", aiResponse)
console.log("Existing: ", existingAIResponse)
const [parsedResponse, setParsedResponse] = useState<AIResponse | null>(existingAIResponse || null);
const [selectedPlatform, setSelectedPlatform] = useState<string>('');
const [editorContent, setEditorContent] = useState<Descendant[]>(initialValue);
const [error, setError] = useState<string | null>(null);
// Parse the AI response when it changes
useEffect(() => {
if (!aiResponse) {
// If there's no new AI response but we have existing data, maintain it
if (existingAIResponse) {
setParsedResponse(existingAIResponse);
// Set the first platform as selected by default if none selected
const platforms = Object.keys(existingAIResponse.platforms);
if (platforms.length > 0 && !selectedPlatform) {
setSelectedPlatform(platforms[0]);
}
}
return;
}
try {
let cleanedResponse = aiResponse.trim()
.replace(/^```json\s*/i, '')
.replace(/\s*```$/g, '');
console.log("Attempting to parse:", cleanedResponse);
// Try to parse the cleaned AI response as JSON
const parsedData = JSON.parse(cleanedResponse) as AIResponse;
// If we have existing data, merge the new data with it
if (existingAIResponse) {
// Merge platforms
const mergedPlatforms = { ...existingAIResponse.platforms };
// Add/update platforms from new response
Object.entries(parsedData.platforms).forEach(([platform, content]) => {
mergedPlatforms[platform] = content;
});
// Create merged response
const mergedData: AIResponse = {
platforms: mergedPlatforms,
generalTips: [
...existingAIResponse.generalTips || [],
...parsedData.generalTips || []
]
};
setParsedResponse(mergedData);
// Pass the full merged response back to the parent component
onChange(editorContent, mergedData);
} else {
setParsedResponse(parsedData);
// Pass the full response back to the parent component
onChange(editorContent, parsedData);
}
// Set the first platform as selected by default if none selected
const platforms = Object.keys(parsedData.platforms);
if (platforms.length > 0 && !selectedPlatform) {
setSelectedPlatform(platforms[0]);
}
setError(null);
} catch (err) {
console.error('Error parsing AI response:', err);
setError('Failed to parse AI response. Please check the format.');
}
}, [aiResponse, existingAIResponse]);
// Update editor content when the selected platform changes
useEffect(() => {
if (!parsedResponse || !selectedPlatform) return;
const platformContent = parsedResponse.platforms[selectedPlatform];
if (platformContent) {
const slateNodes = platformContentToSlateNodes(platformContent);
setEditorContent(slateNodes);
// onChange(slateNodes);
}
}, [selectedPlatform, parsedResponse]);
// Handle platform change
const handlePlatformChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedPlatform(e.target.value);
};
// Handle editor content change
const handleEditorChange = (value: Descendant[], updatedAIResponse: AIResponse | null) => {
setEditorContent(value);
onChange(value, parsedResponse);
};
// useEffect(()=>{
// onChange(editorContent)
// }, [editorContent])
// Get available platforms
const platforms = parsedResponse ? Object.keys(parsedResponse.platforms) : [];
return (
<div>
{error && (
<div className="p-3 mb-4 bg-red-100 text-red-700 rounded-md">
{error}
</div>
)}
{parsedResponse && platforms.length > 0 && (
<div className="mb-4">
<div className="flex items-center gap-3 mb-2">
<label htmlFor="platform-select" className="font-medium">
Select Platform:
</label>
<select
id="platform-select"
value={selectedPlatform}
onChange={handlePlatformChange}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{platforms.map((platform) => (
<option key={platform} value={platform}>
{platform.charAt(0).toUpperCase() + platform.slice(1)}
</option>
))}
</select>
</div>
<RichTextEditor
value={editorContent}
onChange={handleEditorChange}
/>
{/* General Tips Section */}
{parsedResponse.generalTips && parsedResponse.generalTips.length > 0 && (
<div className="mt-4 p-4 bg-gray-50 rounded-md border border-gray-200">
<h3 className="font-medium mb-2">General Tips:</h3>
<ul className="list-disc pl-5">
{parsedResponse.generalTips.map((tip, index) => (
<li key={index} className="mb-1">{tip}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* {(!parsedResponse || platforms.length === 0) && !error && (
<div className="p-4 bg-blue-50 text-blue-700 rounded-md">
No platform-specific content available. Generate content using AI first.
</div>
)} */}
</div>
);
};
export default PlatformSpecificContent;

View File

@ -0,0 +1,468 @@
import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import { createEditor, Descendant, Editor, Transforms, Element as SlateElement, Node, Text, BaseEditor } from 'slate';
import { Slate, Editable, withReact, useSlate, ReactEditor } from 'slate-react';
import { withHistory } from 'slate-history';
import {
Bold, Italic, Underline, List, Link, Image as ImageIcon,
} from 'lucide-react';
import { AIResponse } from './Platform';
// Define allowed block types more strictly
const BLOCK_TYPES = [
'paragraph',
'heading-one',
'heading-two',
'block-quote',
'numbered-list',
'bulleted-list',
'list-item',
'image',
'link'
] as const;
// Define formatting marks
const TEXT_FORMATS = ['bold', 'italic', 'underline'] as const;
type BlockType = typeof BLOCK_TYPES[number];
type TextFormat = typeof TEXT_FORMATS[number];
// Custom types for our editor
type CustomElement = {
type: BlockType;
align?: string;
url?: string;
children: CustomText[];
[key: string]: any; // Allow index signature for dynamic property access
};
type CustomText = {
text: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
[key: string]: any; // Allow index signature for dynamic property access
};
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
const withImages = (editor: Editor) => {
const { isVoid } = editor;
editor.isVoid = element => {
return element.type === 'image' ? true : isVoid(element);
};
return editor;
};
// Define our own custom toolbar button component
const ToolbarButton = ({ active, onMouseDown, children }: {
active: boolean;
onMouseDown: (event: React.MouseEvent) => void;
children: React.ReactNode;
}) => {
return (
<button
className={`p-2 rounded hover:bg-gray-200 ${active ? 'bg-gray-200 text-blue-600' : 'text-gray-700'}`}
onMouseDown={onMouseDown}
>
{children}
</button>
);
};
// Helper to check if current selection has a specific format
const isFormatActive = (editor: Editor, format: TextFormat) => {
const [match] = Editor.nodes(editor, {
match: n => Text.isText(n) && n[format] === true,
mode: 'all',
});
return !!match;
};
// Toggle a format on the current selection
const toggleFormat = (editor: Editor, format: TextFormat) => {
const isActive = isFormatActive(editor, format);
Transforms.setNodes(
editor,
{ [format]: isActive ? null : true },
{ match: n => Text.isText(n), split: true }
);
};
// Check if block is active
const isBlockActive = (editor: Editor, format: BlockType, blockType = 'type') => {
const { selection } = editor;
if (!selection) return false;
const [match] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
});
return !!match;
};
// Toggle block type
const toggleBlock = (editor: Editor, format: BlockType) => {
const isActive = isBlockActive(editor, format);
const isList = ['numbered-list', 'bulleted-list'].includes(format);
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
(['numbered-list', 'bulleted-list'] as BlockType[]).includes(n.type as BlockType),
split: true,
});
const newProperties: Partial<CustomElement> = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
};
Transforms.setNodes<CustomElement>(editor, newProperties);
if (!isActive && isList) {
const block: CustomElement = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
// Insert image
const insertImage = (editor: Editor, url: string) => {
const image: CustomElement = {
type: 'image',
url,
children: [{ text: '' }]
};
Transforms.insertNodes(editor, image);
};
// Insert link
const insertLink = (editor: Editor, url: string) => {
if (!url) return;
const { selection } = editor;
const link: CustomElement = {
type: 'link',
url,
children: selection ? [] : [{ text: url }],
};
if (selection) {
Transforms.wrapNodes(editor, link, { split: true });
} else {
Transforms.insertNodes(editor, link);
}
};
// Format Buttons Components
const FormatButton = ({ format, icon }: { format: TextFormat; icon: React.ReactNode }) => {
const editor = useSlate();
return (
<ToolbarButton
active={isFormatActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleFormat(editor, format);
}}
>
{icon}
</ToolbarButton>
);
};
const BlockButton = ({ format, icon }: { format: BlockType; icon: React.ReactNode }) => {
const editor = useSlate();
return (
<ToolbarButton
active={isBlockActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleBlock(editor, format);
}}
>
{icon}
</ToolbarButton>
);
};
const LinkButton = () => {
const editor = useSlate();
const handleInsertLink = (event: React.MouseEvent) => {
event.preventDefault();
const url = prompt('Enter a URL:');
if (!url) return;
insertLink(editor, url);
};
return (
<ToolbarButton
active={isBlockActive(editor, 'link')}
onMouseDown={handleInsertLink}
>
<Link size={16} />
</ToolbarButton>
);
};
const ImageButton = () => {
const editor = useSlate();
const handleInsertImage = (event: React.MouseEvent) => {
event.preventDefault();
const url = prompt('Enter image URL:');
if (!url) return;
insertImage(editor, url);
};
return (
<ToolbarButton
active={false}
onMouseDown={handleInsertImage}
>
<ImageIcon size={16} />
</ToolbarButton>
);
};
// Our main editor component
const RichTextEditor = ({
value,
onChange
}: {
value: Descendant[];
onChange: (value: Descendant[], updatedAIResponse: AIResponse | null) => void;
}) => {
const renderElement = useCallback((props: any) => <Element {...props} />, []);
const renderLeaf = useCallback((props: any) => <Leaf {...props} />, []);
const editor = useMemo(() => withImages(withHistory(withReact(createEditor()))), []);
const isInitialMount = useRef(true);
// Create local state to track editor value changes
// const [internalValue, setInternalValue] = useState<Descendant[]>([]);
// // Sync internal state with props
// useEffect(() => {
// if (value && value.length > 0) {
// setInternalValue(value);
// // onChange(value)
// }
// }, [value])
console.log(JSON.stringify(value))
useEffect(() => {
// Compare the current editor content with the incoming value
const currentContent = JSON.stringify(editor.children);
const newContent = JSON.stringify(value);
if (currentContent !== newContent) {
// Clear existing content
Transforms.delete(editor, {
at: {
anchor: Editor.start(editor, []),
focus: Editor.end(editor, []),
},
});
// Insert new content
Transforms.insertNodes(editor, value, { at: [0] });
}
}, [value, editor]);
// Handle changes from user input or editor updates
const handleChange = (newValue: Descendant[]) => {
// Only call onChange if the content has actually changed
if (JSON.stringify(newValue) !== JSON.stringify(value)) {
onChange(newValue, null);
}
};
// Handle image paste
const handlePaste = useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
const pastedData = event.clipboardData;
const pastedFiles = pastedData.files;
if (pastedFiles.length === 0) return;
const file = pastedFiles[0];
if (!file.type.match(/^image\/(gif|jpe?g|png)$/i)) return;
event.preventDefault();
const reader = new FileReader();
// Ensure we only insert once
reader.onload = () => {
const url = reader.result as string;
const { selection } = editor;
if (selection) {
insertImage(editor, url);
}
};
reader.readAsDataURL(file);
},
[editor]
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
const { selection } = editor;
if (event.key === 'Enter') {
const [match] = Editor.nodes(editor, {
match: n => SlateElement.isElement(n) && Editor.isVoid(editor, n),
});
if (match) {
event.preventDefault();
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [{ text: '' }],
});
return;
}
}
};
const initialEditorValue = value.length > 0 ? value : initialValue;
return (
<div className="border border-gray-300 rounded-md bg-white">
{/* {internalValue.length > 0 && <Slate editor={editor} initialValue={safeValue} onChange={handleChange}>
<div className="flex flex-wrap items-center p-2 border-b border-gray-300 bg-gray-50">
<FormatButton format="bold" icon={<Bold size={16} />} />
<FormatButton format="italic" icon={<Italic size={16} />} />
<FormatButton format="underline" icon={<Underline size={16} />} />
<div className="w-px h-6 mx-2 bg-gray-300" />
<BlockButton format="bulleted-list" icon={<List size={16} />} />
<LinkButton />
<ImageButton />
</div>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Write your content here..."
className="p-4 min-h-[200px] prose max-w-none"
spellCheck
autoFocus
onKeyDown={handleKeyDown}
onPaste={handlePaste}
/>
</Slate>} */}
<Slate editor={editor} initialValue={initialEditorValue} onChange={handleChange}>
<div className="flex flex-wrap items-center p-2 border-b border-gray-300 bg-gray-50">
<FormatButton format="bold" icon={<Bold size={16} />} />
<FormatButton format="italic" icon={<Italic size={16} />} />
<FormatButton format="underline" icon={<Underline size={16} />} />
<div className="w-px h-6 mx-2 bg-gray-300" />
<BlockButton format="bulleted-list" icon={<List size={16} />} />
<LinkButton />
<ImageButton />
</div>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Write your content here..."
className="p-4 min-h-[200px] prose max-w-none"
spellCheck
autoFocus
onKeyDown={handleKeyDown}
onPaste={handlePaste}
/>
</Slate>
</div>
);
};
// Define a React component renderer for our custom elements
const Element = ({ attributes, children, element }: {
attributes: any;
children: React.ReactNode;
element: CustomElement;
}) => {
switch (element.type) {
case 'paragraph':
return <p {...attributes}>{children}</p>;
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>;
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>;
case 'heading-one':
return <h1 {...attributes}>{children}</h1>;
case 'heading-two':
return <h2 {...attributes}>{children}</h2>;
case 'list-item':
return <li {...attributes}>{children}</li>;
case 'numbered-list':
return <ol {...attributes}>{children}</ol>;
case 'link':
return (
<a
{...attributes}
href={element.url}
className="text-blue-500 underline"
>
{children}
</a>
);
case 'image':
return (
<div {...attributes}>
<div contentEditable={false} className="my-2">
<img
src={element.url}
alt=""
className="max-w-full h-auto rounded"
/>
</div>
{children}
</div>
);
default:
return <p {...attributes}>{children}</p>;
}
};
// Define a React component renderer for text leaf formatting
const Leaf = ({ attributes, children, leaf }: {
attributes: any;
children: React.ReactNode;
leaf: CustomText;
}) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
if (leaf.italic) {
children = <em>{children}</em>;
}
if (leaf.underline) {
children = <u>{children}</u>;
}
return <span {...attributes}>{children}</span>;
};
// Default initial value for the editor
export const initialValue: Descendant[] = [
{
type: 'paragraph' as BlockType,
children: [{ text: '' }],
},
];
export default RichTextEditor;

View File

@ -0,0 +1,59 @@
import React, { forwardRef } from 'react';
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
error?: string;
helperText?: string;
fullWidth?: boolean;
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
(
{
label,
error,
helperText,
fullWidth = false,
className = '',
disabled,
...rest
},
ref
) => {
const baseTextareaStyles =
'flex min-h-[80px] rounded-md border bg-white px-3 py-2 text-sm transition-colors placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:placeholder:text-gray-500 dark:focus-visible:ring-offset-gray-900';
const errorTextareaStyles = error
? 'border-error-500 focus-visible:ring-error-500'
: 'border-gray-300 dark:border-gray-600';
const widthStyle = fullWidth ? 'w-full' : '';
return (
<div className={`${widthStyle} space-y-1 ${className}`}>
{label && (
<label
className="text-sm font-medium leading-none text-gray-900 dark:text-gray-100"
htmlFor={rest.id}
>
{label}
</label>
)}
<textarea
ref={ref}
className={`${baseTextareaStyles} ${errorTextareaStyles} ${widthStyle}`}
disabled={disabled}
{...rest}
/>
{helperText && !error && (
<p className="text-xs text-gray-500 dark:text-gray-400">{helperText}</p>
)}
{error && <p className="text-xs text-error-600">{error}</p>}
</div>
);
}
);
Textarea.displayName = 'Textarea';
export default Textarea;

View File

@ -0,0 +1,315 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { Descendant } from 'slate';
interface UseAIContentStreamProps {
onContentUpdate?: (content: string) => void;
onComplete?: (fullContent: string) => void;
onError?: (error: string) => void;
}
export const useAIContentStream = ({
onContentUpdate,
onComplete,
onError
}: UseAIContentStreamProps) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [streamedContent, setStreamedContent] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const generateContent = async (prompt: string) => {
try {
setIsLoading(true);
setStreamedContent('');
setError(null);
// Close any existing connections
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// Create a full text accumulator
let fullText = '';
// Create new EventSource connection for SSE
const url = 'http://localhost:3000/v1/ideas/AIIdeaContent';
// First make a POST request to start the streaming
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('token') || ''
},
body: JSON.stringify({ prompt }),
});
if (!response.ok) {
throw new Error('Failed to connect to AI service');
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Response body reader could not be created');
}
// Read the stream
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// Decode the chunk
const chunk = decoder.decode(value, { stream: true });
// Parse SSE format (data: {...}\n\n)
const lines = chunk.split('\n\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.substring(6));
if (eventData.error) {
throw new Error(eventData.error);
}
if (eventData.done) {
// Stream completed
console.log(fullText)
if (onComplete) {
onComplete(fullText);
}
continue;
}
if (eventData.text) {
fullText += eventData.text;
setStreamedContent(fullText);
// console.log("Full text", fullText)
if (onContentUpdate) {
onContentUpdate(fullText);
}
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
setError(errorMessage);
if (onError) {
onError(errorMessage);
}
} finally {
setIsLoading(false);
}
};
const cancelGeneration = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
setIsLoading(false);
};
useEffect(() => {
// Cleanup function to close EventSource when component unmounts
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
}, []);
return {
generateContent,
cancelGeneration,
isLoading,
streamedContent,
error
};
};
// Helper function to convert plain text to Slate format
// export const textToSlateNodes = (text: string): Descendant[] => {
// try {
// const jsonObj = JSON.parse(text);
// // If this is our platform-specific JSON format, return empty array
// // as we'll handle this specially in PlatformSpecificContent
// if (jsonObj.platforms) {
// return [];
// }
// } catch (e) {
// // Not valid JSON, continue with normal text processing
// }
// // If no text, return a single empty paragraph
// if (!text || text.trim() === '') {
// return [{
// type: 'paragraph',
// children: [{ text: '' }]
// }];
// }
// console.log("Text: ", text)
// // Split by double newlines to create paragraphs
// const paragraphs = text.split(/\n\n+/);
// // Map paragraphs to Slate nodes
// const nodes: Descendant[] = paragraphs.map(paragraph => {
// // Trim the paragraph and handle empty paragraphs
// const trimmedParagraph = paragraph.trim();
// // If paragraph is empty after trimming, return an empty paragraph
// if (trimmedParagraph === '') {
// return {
// type: 'paragraph',
// children: [{ text: '' }]
// };
// }
// // Handle multiple lines within a paragraph
// const lines = trimmedParagraph.split(/\n/);
// // If multiple lines, create a paragraph with line breaks
// if (lines.length > 1) {
// return {
// type: 'paragraph',
// children: lines.flatMap((line, index) => [
// { text: line },
// // Add a soft line break between lines, except for the last line
// ...(index < lines.length - 1 ? [{ text: '\n' }] : [])
// ])
// };
// }
// // Single line paragraph
// return {
// type: 'paragraph',
// children: [{ text: trimmedParagraph }]
// };
// });
// // Ensure at least one paragraph exists
// return nodes.length > 0 ? nodes : [{
// type: 'paragraph',
// children: [{ text: '' }]
// }];
// };
// Helper function for cleaning AI response JSON
export const cleanAIResponse = (response: string): string => {
if (!response) return '';
// Remove markdown code block markers if present
let cleanedResponse = response.trim();
// Remove opening code block identifier
cleanedResponse = cleanedResponse.replace(/^```(?:json)?\s*/i, '');
// Remove closing code block marker
cleanedResponse = cleanedResponse.replace(/\s*```$/g, '');
return cleanedResponse;
};
// Function to safely parse JSON with better error handling
export const safeParseJSON = <T>(jsonString: string): { success: boolean; data: T | null; error: string | null } => {
try {
// Clean the string first
const cleanedString = cleanAIResponse(jsonString);
// Try to parse
const parsedData = JSON.parse(cleanedString) as T;
return {
success: true,
data: parsedData,
error: null
};
} catch (err) {
console.error("JSON parsing error:", err);
return {
success: false,
data: null,
error: err instanceof Error ? err.message : String(err)
};
}
};
// Enhanced version of your textToSlateNodes function
export const textToSlateNodes = (text: string): Descendant[] => {
if (!text) {
return [{
type: 'paragraph',
children: [{ text: '' }]
}];
}
// Try to parse as JSON first
const parseResult = safeParseJSON<any>(text);
// If it's a valid JSON with platforms property, return empty array
// as we'll handle this specially in PlatformSpecificContent
if (parseResult.success && parseResult.data && parseResult.data.platforms) {
return [];
}
// Continue with normal text processing for non-JSON content
const cleanedText = cleanAIResponse(text);
// If no valid text after cleaning, return a single empty paragraph
if (!cleanedText || cleanedText.trim() === '') {
return [{
type: 'paragraph',
children: [{ text: '' }]
}];
}
// Split by double newlines to create paragraphs
const paragraphs = cleanedText.split(/\n\n+/);
// Map paragraphs to Slate nodes
const nodes: Descendant[] = paragraphs.map(paragraph => {
const trimmedParagraph = paragraph.trim();
if (trimmedParagraph === '') {
return {
type: 'paragraph',
children: [{ text: '' }]
};
}
// Handle multiple lines within a paragraph
const lines = trimmedParagraph.split(/\n/);
if (lines.length > 1) {
return {
type: 'paragraph',
children: lines.flatMap((line, index) => [
{ text: line },
...(index < lines.length - 1 ? [{ text: '\n' }] : [])
])
};
}
return {
type: 'paragraph',
children: [{ text: trimmedParagraph }]
};
});
return nodes.length > 0 ? nodes : [{
type: 'paragraph',
children: [{ text: '' }]
}];
};

View File

@ -0,0 +1,146 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { AuthState } from '../types';
import axios from "axios"
interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
register: (name: string, email: string, password: string) => Promise<void>;
logout: () => void;
resetPassword: (email: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [authState, setAuthState] = useState<AuthState>({
token: null,
isAuthenticated: false,
isLoading: true,
});
useEffect(() => {
const checkAuthStatus = () => {
const token = localStorage.getItem('token');
if (token) {
try {
setAuthState({
token: token,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
localStorage.removeItem('token');
localStorage.removeItem('user');
setAuthState({
token: null,
isAuthenticated: false,
isLoading: false,
});
}
} else {
setAuthState({
token: null,
isAuthenticated: false,
isLoading: false,
});
}
};
checkAuthStatus();
}, []);
// In a real app, you would use API calls to a backend for these functions
const login = async (email: string, password: string) => {
const response = await axios.post("http://localhost:3000/v1/auth/login", {
email,
password
})
setAuthState((prev) => ({ ...prev, isLoading: true }));
try {
if(response.data.success){
localStorage.setItem('token', "Bearer " + response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
setAuthState({
token: response.data.token,
isAuthenticated: true,
isLoading: false,
});
}
} catch (error) {
setAuthState((prev) => ({ ...prev, isLoading: false }));
throw new Error('Login failed');
}
};
const register = async (name: string, email: string, password: string) => {
const response = await axios.post("http://localhost:3000/v1/auth/register", {
name,
email,
password
})
setAuthState((prev) => ({ ...prev, isLoading: true }));
try {
if(response.data.success){
localStorage.setItem('token', "Bearer " + response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
setAuthState({
token: response.data.token,
isAuthenticated: true,
isLoading: false,
});
}
} catch (error) {
setAuthState((prev) => ({ ...prev, isLoading: false }));
throw new Error('Registration failed');
}
};
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
setAuthState({
token: null,
isAuthenticated: false,
isLoading: false,
});
};
const resetPassword = async (email: string) => {
try {
console.log(`Password reset email sent to: ${email}`);
} catch (error) {
throw new Error('Password reset failed');
}
};
return (
<AuthContext.Provider
value={{
...authState,
login,
register,
logout,
resetPassword,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@ -0,0 +1,345 @@
import { createContext, useState, useContext, ReactNode, useEffect } from "react";
import { EventInput, DateSelectArg } from '@fullcalendar/core';
import axios from 'axios';
// Define the Event type that matches our Prisma schema
export type CalendarEvent = {
id: string;
title: string;
start: Date | string;
end?: Date | string;
allDay: boolean;
description?: string;
color?: string;
userId: number;
ideaIds?: string[]; // IDs of linked ideas
createdAt?: Date;
updatedAt?: Date;
};
// Define what our context will provide
type EventsContextType = {
events: CalendarEvent[];
loading: boolean;
error: string | null;
fetchEvents: () => Promise<void>;
addEvent: (event: Omit<CalendarEvent, 'id'| "userId">) => Promise<CalendarEvent | null>;
updateEvent: (updatedEvent: CalendarEvent) => Promise<boolean>;
deleteEvent: (eventId: string) => Promise<boolean>;
getEventsByIdeaId: (ideaId: string) => CalendarEvent[];
linkEventToIdea: (eventId: string, ideaId: string) => Promise<boolean>;
unlinkEventFromIdea: (eventId: string, ideaId: string) => Promise<boolean>;
// Functions for FullCalendar integration
getEventsForFullCalendar: () => EventInput[];
};
// Create the context with default values
const EventsContext = createContext<EventsContextType | undefined>(undefined);
// Helper function to create EventInput objects for FullCalendar
const convertToEventInput = (event: CalendarEvent): EventInput => {
return {
id: event.id,
title: event.title,
start: event.start,
end: event.end,
allDay: event.allDay,
backgroundColor: event.color,
extendedProps: {
description: event.description,
userId: event.userId,
ideaIds: event.ideaIds
}
};
};
// Create a provider component
export const EventsProvider = ({ children }: { children: ReactNode }) => {
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// Fetch events from API
const fetchEvents = async () => {
setLoading(true);
setError(null);
try {
const response = await axios.get('http://localhost:3000/v1/events/getEvents', {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
// Ensure dates are properly parsed from strings
const parsedEvents = response.data.data.map((event: any) => ({
...event,
start: new Date(event.start),
end: event.end ? new Date(event.end) : undefined,
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
updatedAt: event.updatedAt ? new Date(event.updatedAt) : undefined
}));
setEvents(parsedEvents);
} else {
setError("Failed to fetch events");
}
} catch (err) {
console.error("Error fetching events:", err);
setError("Failed to fetch events");
} finally {
setLoading(false);
}
};
// Load events when the component mounts
useEffect(() => {
fetchEvents();
}, []);
// Add an event
const addEvent = async (event: Omit<CalendarEvent, 'id' | 'userId'>): Promise<CalendarEvent | null> => {
try {
// Ensure we're sending properly formatted date strings
const formattedEvent = {
...event,
start: formatDateString(event.start),
end: event.end ? formatDateString(event.end) : undefined
};
console.log('Sending event to API:', formattedEvent);
const response = await axios.post('http://localhost:3000/v1/events/createEvent', formattedEvent, {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
const newEvent = {
...response.data.data,
start: new Date(response.data.data.start),
end: response.data.data.end ? new Date(response.data.data.end) : undefined,
createdAt: response.data.data.createdAt ? new Date(response.data.data.createdAt) : undefined,
updatedAt: response.data.data.updatedAt ? new Date(response.data.data.updatedAt) : undefined
};
await fetchEvents()
} else {
// Log more detailed error information
console.error("API error creating event:", response.data);
setError(response.data.message || "Failed to create event");
}
return null;
} catch (err: any) {
console.error("Error creating event:", err);
if (err.response) {
console.error("API response error:", err.response.data);
setError(err.response.data.message || "Failed to create event");
} else {
setError("Failed to create event");
}
return null;
}
};
// Helper function to ensure consistent date formatting
const formatDateString = (date: Date | string): string => {
if (typeof date === 'string') {
try {
// Attempt to parse the string as a date and format it
return new Date(date).toISOString();
} catch (e) {
console.error('Invalid date string:', date);
return date; // Return the original if parsing fails
}
}
return date.toISOString();
};
// Update an event
const updateEvent = async (updatedEvent: CalendarEvent): Promise<boolean> => {
try {
// Ensure we're sending properly formatted date strings
const formattedEvent = {
...updatedEvent,
start: formatDateString(updatedEvent.start),
end: updatedEvent.end ? formatDateString(updatedEvent.end) : undefined
};
console.log('Sending updated event to API:', formattedEvent);
const response = await axios.put(
`http://localhost:3000/v1/events/updateEvent/${updatedEvent.id}`,
formattedEvent, {
headers: { authorization: localStorage.getItem("token") }
}
);
if (response.data.success) {
const updated = {
...response.data.data,
start: new Date(response.data.data.start),
end: response.data.data.end ? new Date(response.data.data.end) : undefined,
updatedAt: new Date()
};
await fetchEvents()
} else {
// Log more detailed error information
console.error("API error updating event:", response.data);
setError(response.data.message || "Failed to update event");
}
return false;
} catch (err: any) {
console.error("Error updating event:", err);
if (err.response) {
console.error("API response error:", err.response.data);
setError(err.response.data.message || "Failed to update event");
} else {
setError("Failed to update event");
}
return false;
}
};
// Delete an event
const deleteEvent = async (eventId: string): Promise<boolean> => {
try {
const response = await axios.delete(`http://localhost:3000/v1/events/deleteEvent/${eventId}`, {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
await fetchEvents()
return true
}
return false;
} catch (err) {
console.error("Error deleting event:", err);
setError("Failed to delete event");
return false;
}
};
// Get events linked to a specific idea
const getEventsByIdeaId = (ideaId: string): CalendarEvent[] => {
return events.filter(event =>
event.ideaIds && event.ideaIds.includes(ideaId)
);
};
// Link an event to an idea
const linkEventToIdea = async (eventId: string, ideaId: string): Promise<boolean> => {
try {
const response = await axios.post('http://localhost:3000/v1/events/linkEventToIdea', {
eventId,
ideaId
}, {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
// Update local state
setEvents(prev => prev.map(event => {
if (event.id === eventId) {
const ideaIds = event.ideaIds || [];
if (!ideaIds.includes(ideaId)) {
return {
...event,
ideaIds: [...ideaIds, ideaId],
updatedAt: new Date()
};
}
}
return event;
}));
return true;
}
return false;
} catch (err) {
console.error("Error linking event to idea:", err);
setError("Failed to link event to idea");
return false;
}
};
// Unlink an event from an idea
const unlinkEventFromIdea = async (eventId: string, ideaId: string): Promise<boolean> => {
try {
const response = await axios.post('http://localhost:3000/v1/events/unlinkEventFromIdea', {
eventId,
ideaId
}, {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
setEvents(prev => prev.map(event => {
if (event.id === eventId && event.ideaIds) {
return {
...event,
ideaIds: event.ideaIds.filter(id => id !== ideaId),
updatedAt: new Date()
};
}
return event;
}));
return true;
}
// Fallback implementation if API endpoint doesn't exist yet
setEvents(prev => prev.map(event => {
if (event.id === eventId && event.ideaIds) {
return {
...event,
ideaIds: event.ideaIds.filter(id => id !== ideaId),
updatedAt: new Date()
};
}
return event;
}));
return true;
} catch (err) {
console.error("Error unlinking event from idea:", err);
// Even if API fails, update local state
setEvents(prev => prev.map(event => {
if (event.id === eventId && event.ideaIds) {
return {
...event,
ideaIds: event.ideaIds.filter(id => id !== ideaId),
updatedAt: new Date()
};
}
return event;
}));
setError("Failed to unlink event from idea on server, but updated locally");
return true;
}
};
// Get events formatted for FullCalendar
const getEventsForFullCalendar = (): EventInput[] => {
return events.map(convertToEventInput);
};
const value = {
events,
loading,
error,
fetchEvents,
addEvent,
updateEvent,
deleteEvent,
getEventsByIdeaId,
linkEventToIdea,
unlinkEventFromIdea,
getEventsForFullCalendar
};
return <EventsContext.Provider value={value}>{children}</EventsContext.Provider>;
};
// Create a custom hook to use the context
export const useEvents = () => {
const context = useContext(EventsContext);
if (context === undefined) {
throw new Error("useEvents must be used within an EventsProvider");
}
return context;
};

View File

@ -0,0 +1,244 @@
// import { createContext, useState, useContext, ReactNode } from "react";
// // Define the Idea type
// export type Idea = {
// id: string;
// title: string;
// status: string;
// tags: string[];
// content: string;
// };
// // Define the status options available in the application
// export type StatusOption = {
// label: string;
// color: string;
// };
// export const statusOptions: StatusOption[] = [
// { label: "Not Started", color: "bg-gray-500" },
// { label: "In Progress", color: "bg-yellow-600" },
// { label: "Completed", color: "bg-green-600" }
// ];
// // Define what our context will provide
// type IdeasContextType = {
// ideas: Idea[];
// addIdea: (idea: Idea) => void;
// updateIdea: (idea: Idea) => void;
// deleteIdea: (ideaId: string) => void;
// getStatusColor: (status: string) => string;
// getTagColor: (tag: string) => string;
// };
// // Create the context with default values
// const IdeasContext = createContext<IdeasContextType | undefined>(undefined);
// // Create a provider component
// export const IdeasProvider = ({ children }: { children: ReactNode }) => {
// const [ideas, setIdeas] = useState<Idea[]>([]);
// const addIdea = (idea: Idea) => {
// setIdeas([idea, ...ideas]);
// };
// const updateIdea = (updatedIdea: Idea) => {
// setIdeas(ideas.map(idea =>
// idea.id === updatedIdea.id ? updatedIdea : idea
// ));
// };
// const deleteIdea = (ideaId: string) => {
// setIdeas(ideas.filter(idea => idea.id !== ideaId));
// };
// // Get status color based on status label
// const getStatusColor = (status: string): string => {
// return statusOptions.find(option => option.label === status)?.color || "bg-gray-500";
// };
// // Get a color for a tag using a consistent hash
// const getTagColor = (tag: string): string => {
// const colors: string[] = [
// "bg-blue-500", "bg-yellow-500", "bg-green-500",
// "bg-red-500", "bg-purple-500", "bg-pink-500",
// "bg-indigo-500", "bg-orange-500"
// ];
// // Simple hash function to consistently assign the same color to the same tag
// let hash = 0;
// for (let i = 0; i < tag.length; i++) {
// hash = tag.charCodeAt(i) + ((hash << 7) - hash);
// }
// return colors[Math.abs(hash) % colors.length];
// };
// const value = {
// ideas,
// addIdea,
// updateIdea,
// deleteIdea,
// getStatusColor,
// getTagColor
// };
// return <IdeasContext.Provider value={value}>{children}</IdeasContext.Provider>;
// };
// // Create a custom hook to use the context
// export const useIdeas = () => {
// const context = useContext(IdeasContext);
// if (context === undefined) {
// throw new Error("useIdeas must be used within an IdeasProvider");
// }
// return context;
// };
import axios from "axios";
import { createContext, useState, useContext, ReactNode, useEffect } from "react";
import { Descendant } from "slate";
import { AIResponse } from "../components/ui/Platform";
export type Idea = {
id: string;
title: string;
status: string;
tags: string[];
content: Descendant[];
userId: number;
platformContent?: AIResponse;
createdAt?: Date;
updatedAt?: Date;
};
export type StatusOption = {
label: string;
color: string;
};
export const statusOptions: StatusOption[] = [
{ label: "Not Started", color: "bg-gray-500" },
{ label: "In Progress", color: "bg-yellow-600" },
{ label: "Completed", color: "bg-green-600" }
];
type IdeasContextType = {
ideas: Idea[];
fetchIdeas: () => void;
addIdea: (idea: Omit<Idea, 'id' | 'createdAt' | 'updatedAt'>) => Promise<void>;
updateIdea: (idea: Idea) => Promise<void>;
deleteIdea: (ideaId: string) => Promise<void>;
getStatusColor: (status: string) => string;
getTagColor: (tag: string) => string;
getUserIdeas: (userId: number) => Idea[];
};
const IdeasContext = createContext<IdeasContextType | undefined>(undefined);
export const IdeasProvider = ({ children }: { children: ReactNode }) => {
const [ideas, setIdeas] = useState<Idea[]>([]);
const fetchIdeas = async () => {
try {
const response = await axios.get("http://localhost:3000/v1/ideas/getIdeas", {
headers: { authorization: localStorage.getItem("token") }
});
if (response.data.success) {
setIdeas(response.data.data);
}
} catch (error) {
console.error("Failed to fetch ideas", error);
}
};
useEffect(() => {
fetchIdeas();
}, []);
const addIdea = async (idea: Omit<Idea, 'id' | "userId" | 'createdAt' | 'updatedAt'>) => {
try {
await axios.post("http://localhost:3000/v1/ideas/createIdea", {
...idea,
platformContent: idea.platformContent || null}, {
headers: { authorization: localStorage.getItem("token") }
});
await fetchIdeas();
} catch (error) {
console.error("Failed to add idea", error);
}
};
const updateIdea = async (updatedIdea: Idea) => {
try {
const response = await axios.put(`http://localhost:3000/v1/ideas/updateIdea/${updatedIdea.id}`, {
title: updatedIdea.title,
status: updatedIdea.status,
tags: updatedIdea.tags,
content: updatedIdea.content,
platformContent: updatedIdea.platformContent || null
}, {
headers: { authorization: localStorage.getItem("token") }
});
if(response.data.success){
await fetchIdeas();
}
} catch (error) {
console.error("Failed to update idea", error);
}
};
const deleteIdea = async (ideaId: string) => {
try {
await axios.delete(`http://localhost:3000/v1/ideas/deleteIdea/${ideaId}`, {
headers: { authorization: localStorage.getItem("token") }
});
await fetchIdeas();
} catch (error) {
console.error("Failed to delete idea", error);
}
};
const getUserIdeas = (userId: number): Idea[] => {
return ideas.filter(idea => idea.userId === userId);
};
const getStatusColor = (status: string): string => {
return statusOptions.find(option => option.label === status)?.color || "bg-gray-500";
};
const getTagColor = (tag: string): string => {
const colors: string[] = [
"bg-blue-500", "bg-yellow-500", "bg-green-500",
"bg-red-500", "bg-purple-500", "bg-pink-500",
"bg-indigo-500", "bg-orange-500"
];
let hash = 0;
for (let i = 0; i < tag.length; i++) {
hash = tag.charCodeAt(i) + ((hash << 7) - hash);
}
return colors[Math.abs(hash) % colors.length];
};
const value: IdeasContextType = {
ideas,
fetchIdeas,
addIdea,
updateIdea,
deleteIdea,
getStatusColor,
getTagColor,
getUserIdeas
};
return <IdeasContext.Provider value={value}>{children}</IdeasContext.Provider>;
};
export const useIdeas = () => {
const context = useContext(IdeasContext);
if (context === undefined) {
throw new Error("useIdeas must be used within an IdeasProvider");
}
return context;
};

View File

@ -0,0 +1,57 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { Theme } from '../types';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(() => {
// Check local storage or system preference
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
return savedTheme;
}
// Check system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
// Update document class when theme changes
const root = window.document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Save to local storage
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View File

@ -0,0 +1,85 @@
@import "tailwindcss";
@theme {
--primary-50: #1e55ff;
--primary-100: #e0edff;
--primary-200: #c0daff;
--primary-300: #92bfff;
--primary-400: #5c9aff;
--primary-500: #3374ff;
--primary-600: #1e55ff;
--primary-700: #1342f1;
--primary-800: #1635d1;
--primary-900: #1733a5;
--primary-950: #101f5e;
}
/* :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
} */

View File

@ -0,0 +1,24 @@
import React from 'react';
import {Topbar} from './Topbar';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-white flex flex-col transition-colors duration-200">
<Topbar />
<main className="flex-grow">{children}</main>
<footer className="bg-white border-t border-gray-200 py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-center text-sm text-gray-500 dark:text-gray-400">
&copy; {new Date().getFullYear()} CreatorHub. All rights reserved.
</p>
</div>
</footer>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
interface ProtectedRouteProps {
children?: React.ReactNode;
redirectTo?: string;
}
/**
* ProtectedRoute component that redirects unauthenticated users
* Can be used either with children or as a wrapper for route elements
*/
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
redirectTo = '/login'
}) => {
const { isAuthenticated, isLoading } = useAuth();
// Show loading indicator while checking authentication
if (isLoading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
// Redirect if not authenticated
if (!isAuthenticated) {
return <Navigate to={redirectTo} replace />;
}
// Render children if provided, otherwise render the outlet for nested routes
return <>{children ? children : <Outlet />}</>;
};
export default ProtectedRoute;

View File

@ -0,0 +1,21 @@
import { useNavigate } from "react-router-dom"
export const Topbar = () => {
const navigate = useNavigate()
return <div className="px-10 py-2 border-gray-400 shadow-md">
<div className='flex justify-between text-slate-600 border-gray-300 px-5 py-4 '>
<div className="flex gap-x-4 items-center">
<div className="font-bold text-xl">
CreatorHub
</div>
<button className="font-medium text-slate-600 hover:text-black cursor-pointer" onClick={()=>navigate("/")}>Home</button>
<button className="font-medium text-slate-600 hover:text-black cursor-pointer" onClick={()=>navigate("/ideas")}>Ideas</button>
<button className="font-medium text-slate-600 hover:text-black cursor-pointer" onClick={()=>navigate("/schedule")}>Schedule</button>
</div>
<div className="flex gap-x-4 font-medium text-slate-600">
<button className="hover:text-black cursor-pointer" onClick={()=>navigate("/login")}>Login</button>
<button className="hover:text-black cursor-pointer" onClick={()=>navigate("/register")}>Register</button>
</div>
</div>
</div>
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,20 @@
import { Features } from "../components/ui/Features"
import { Hero1 } from "../components/ui/HeroSection1"
export const Landing = () => {
return <div className="">
<div className="mx-10 pb-10">
<Hero1></Hero1>
{/* <div className="ml-5">
<Button className="" size="lg">Start Now</Button>
</div> */}
</div>
<Features />
</div>
}
{/* <div className='flex justify-center'>
<div className='flex items-center'>
<input placeholder="Idea..." className='border rounded px-1' />
<button className='border rounded mx-2 px-2 hover:bg-black hover:text-white'>Add</button>
</div>
</div> */}

View File

@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Mail, Lock } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import { useAuth } from '../../context/AuthContext';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login, isLoading } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
await login(email, password);
navigate('/');
} catch (err) {
setError('Invalid email or password. Please try again.');
}
};
return (
<div className="min-h-[calc(100vh-10rem)] flex items-center justify-center py-16 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-slate-900">Welcome back</h1>
<p className="mt-2 text-slate-600">
Sign in to your account to continue
</p>
</div>
<Card className="animate-scale-up">
<CardHeader>
<CardTitle>Sign in</CardTitle>
<CardDescription>
Enter your credentials to access your account
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="bg-error-50 dark:bg-error-900/20 text-error-800 dark:text-error-200 p-3 rounded-md text-sm">
{error}
</div>
)}
<Input
id="email"
type="email"
label="Email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
fullWidth
leftIcon={<Mail size={16} />}
/>
<Input
id="password"
type="password"
label="Password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
fullWidth
leftIcon={<Lock size={16} />}
/>
{/* <div className="text-right">
<Link
to="/forgot-password"
className="text-sm font-medium text-(--primary-600) hover:text-(--primary-700) dark:text-(--primary-400) dark:hover:text-(--primary-300)"
>
Forgot password?
</Link>
</div> */}
</CardContent>
<CardFooter className="flex flex-col space-y-4">
<Button
type="submit"
fullWidth
isLoading={isLoading}
>
Sign in
</Button>
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
Don't have an account?{' '}
<Link
to="/register"
className="font-medium text-(--primary-600) hover:text-(--primary-700) dark:text-(--primary-400) dark:hover:text-(--primary-300)"
>
Sign up
</Link>
</p>
</CardFooter>
</form>
</Card>
</div>
</div>
);
};
export default Login;

View File

@ -0,0 +1,135 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { User, Mail, Lock } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import { useAuth } from '../../context/AuthContext';
const Register: React.FC = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const { register, isLoading } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
try {
await register(name, email, password);
navigate('/');
} catch (err) {
setError('Registration failed. Please try again.');
}
};
return (
<div className="min-h-[calc(100vh-10rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-slate-900 ">Create an account</h1>
<p className="mt-2 text-slate-600">
Join CreatorHub and start bringing your ideas to life
</p>
</div>
<Card className="animate-scale-up">
<CardHeader>
<CardTitle>Sign up</CardTitle>
<CardDescription>
Enter your information to create an account
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="bg-error-50 dark:bg-error-900/20 text-error-800 dark:text-error-200 p-3 rounded-md text-sm">
{error}
</div>
)}
<Input
id="name"
type="text"
label="Full Name"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
required
fullWidth
leftIcon={<User size={16} />}
/>
<Input
id="email"
type="email"
label="Email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
fullWidth
leftIcon={<Mail size={16} />}
/>
<Input
id="password"
type="password"
label="Password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
fullWidth
leftIcon={<Lock size={16} />}
helperText="Password must be at least 8 characters"
minLength={8}
/>
<Input
id="confirmPassword"
type="password"
label="Confirm Password"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
fullWidth
leftIcon={<Lock size={16} />}
/>
</CardContent>
<CardFooter className="flex flex-col space-y-4">
<Button
type="submit"
fullWidth
isLoading={isLoading}
>
Create account
</Button>
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
Already have an account?{' '}
<Link
to="/login"
className="font-medium text-(--primary-600) hover:text-(--primary-800)"
>
Sign in
</Link>
</p>
</CardFooter>
</form>
</Card>
</div>
</div>
);
};
export default Register;

View File

@ -0,0 +1,120 @@
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { PlusCircle, Search } from "lucide-react";
import { Idea, useIdeas } from "../../context/IdeasContext";
import IdeaCard from "../../components/ui/IdeaCard";
import CreateIdea from "../../components/ui/CreateIdea";
import { Node, Descendant } from "slate"
const getPlainTextFromNodes = (nodes: Descendant[]): string => {
return nodes
.map(node => Node.string(node))
.join(" ");
};
export const Ideas = () => {
const { ideas } = useIdeas();
const [showSearchInput, setShowSearchInput] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [ideaToEdit, setIdeaToEdit] = useState<Idea | null>(null);
// Filter ideas based on search term
const filteredIdeas = ideas.filter(idea => {
if (!searchTerm) return true;
const searchTermLower = searchTerm.toLowerCase();
// const contentText = getPlainTextFromNodes(idea.content);
return (
idea.title.toLowerCase().includes(searchTermLower)
// contentText.toLowerCase().includes(searchTermLower) ||
// idea.tags.some(tag => tag.toLowerCase().includes(searchTermLower))
);
});
const handleOpenEditForm = (idea: Idea) => {
setIdeaToEdit(idea);
setShowCreateForm(true);
};
const handleCloseForm = () => {
setShowCreateForm(false);
setIdeaToEdit(null);
};
return (
<div className="relative h-screen">
{showCreateForm && (
<CreateIdea
onClose={handleCloseForm}
ideaToEdit={ideaToEdit}
/>
)}
<div className="text-5xl ml-15 mt-10 font-bold">
My Ideas
</div>
<div className="border-b mx-15 my-5 pb-2 flex border-gray-300 items-center justify-between">
<div></div>
<div className="flex items-center gap-x-3">
<div
className="hover:bg-gray-200 p-1 rounded-lg cursor-pointer"
onClick={() => setShowSearchInput(!showSearchInput)}
>
<Search className="text-gray-400 transition-all" size={20} />
</div>
<AnimatePresence>
{showSearchInput && (
<motion.input
className="flex py-2 bg-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring- focus-visible:ring-primary-500 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700"
key="input"
placeholder="Type to search..."
autoFocus
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
initial={{ width: 0, opacity: 0 }}
animate={{ width: 200, opacity: 1 }}
transition={{ duration: 0.3 }}
exit={{ width: 0 }}
/>
)}
</AnimatePresence>
<button
className="flex items-center gap-x-2 bg-slate-700 text-white rounded p-2"
onClick={() => {
setIdeaToEdit(null);
setShowCreateForm(true);
}}
>
<PlusCircle strokeWidth={1} size={17} />
New Idea
</button>
</div>
</div>
<div className="mt-6 mx-6">
{filteredIdeas.length > 0 ? (
<div className="grid grid-cols-4 gap-x-2">
{filteredIdeas.map((idea) => (
<IdeaCard
key={idea.id}
idea={idea}
onEdit={handleOpenEditForm}
/>
))}
</div>
) : (
<div className="text-center text-gray-500 mt-10">
{searchTerm ? "No ideas match your search." : "No ideas yet. Create your first idea!"}
</div>
)}
</div>
</div>
);
};
export default Ideas;

View File

@ -0,0 +1,672 @@
// import { useState, useMemo, useEffect } from "react";
// import { Calendar, momentLocalizer, ToolbarProps, View } from "react-big-calendar";
// import moment from "moment";
// import "react-big-calendar/lib/css/react-big-calendar.css";
// import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, X, Clock } from "lucide-react";
// import { useIdeas } from "../../context/IdeasContext";
// import { Node, Descendant } from "slate";
// // Configure localizer for react-big-calendar
// const localizer = momentLocalizer(moment);
// // Helper function to get plain text from Slate nodes (copied from Ideas.jsx)
// const getPlainTextFromNodes = (nodes: Descendant[] | undefined): string => {
// if (!nodes || !Array.isArray(nodes)) return "";
// return nodes.map(node => Node.string(node)).join(" ");
// };
// interface Idea {
// id: string;
// title: string;
// content: Descendant[];
// tags: string[];
// }
// interface CalendarEvent {
// id: string;
// title: string;
// start: Date;
// end: Date;
// ideaId: string;
// allDay?: boolean;
// }
// interface SlotInfo {
// start: Date;
// end: Date;
// slots: Date[];
// action: "select" | "click" | "doubleClick";
// }
// const Schedule = () => {
// const { ideas } = useIdeas();
// const [events, setEvents] = useState<CalendarEvent[]>([]);
// const [selectedDate, setSelectedDate] = useState<Date>(new Date());
// const [showModal, setShowModal] = useState<boolean>(false);
// const [selectedSlot, setSelectedSlot] = useState<SlotInfo | null>(null);
// const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
// const [selectedIdea, setSelectedIdea] = useState<Idea | null>(null);
// const [searchTerm, setSearchTerm] = useState<string>("");
// const [duration, setDuration] = useState<number>(60); // Default duration in minutes
// // Load events from localStorage on component mount
// useEffect(() => {
// const savedEvents = localStorage.getItem("scheduledEvents");
// if (savedEvents) {
// const parsedEvents = JSON.parse(savedEvents).map((event: any) => ({
// ...event,
// start: new Date(event.start),
// end: new Date(event.end)
// }));
// setEvents(parsedEvents);
// }
// }, []);
// // Save events to localStorage whenever events change
// useEffect(() => {
// localStorage.setItem("scheduledEvents", JSON.stringify(events));
// }, [events]);
// // Filter ideas based on search term
// const filteredIdeas = useMemo(() => {
// return ideas.filter((idea: Idea) => {
// if (!searchTerm) return true;
// const searchTermLower = searchTerm.toLowerCase();
// const contentText = getPlainTextFromNodes(idea.content);
// return (
// idea.title.toLowerCase().includes(searchTermLower) ||
// contentText.toLowerCase().includes(searchTermLower) ||
// (idea.tags && idea.tags.some(tag => tag.toLowerCase().includes(searchTermLower)))
// );
// });
// }, [ideas, searchTerm]);
// // Handle slot selection (clicking on a time slot in the calendar)
// const handleSelectSlot = (slotInfo: SlotInfo) => {
// setSelectedSlot(slotInfo);
// setSelectedEvent(null);
// setShowModal(true);
// };
// // Handle event selection (clicking on an existing event)
// const handleSelectEvent = (event: CalendarEvent) => {
// setSelectedEvent(event);
// setSelectedIdea(ideas.find((idea: Idea) => idea.id === event.ideaId) || null);
// setSelectedSlot(null);
// setShowModal(true);
// };
// // Create a new event (schedule an idea)
// const handleCreateEvent = () => {
// if (!selectedIdea || !selectedSlot) return;
// const startTime = selectedSlot.start;
// const endTime = new Date(startTime.getTime() + (duration * 60000)); // Convert minutes to milliseconds
// const newEvent = {
// id: Date.now().toString(),
// title: selectedIdea.title,
// start: startTime,
// end: endTime,
// ideaId: selectedIdea.id,
// allDay: selectedSlot.slots && selectedSlot.slots.length === 1 && selectedSlot.action === "click"
// };
// setEvents([...events, newEvent]);
// setShowModal(false);
// setSelectedSlot(null);
// setSelectedIdea(null);
// };
// // Delete an existing event
// const handleDeleteEvent = () => {
// if (!selectedEvent) return;
// setEvents(events.filter(event => event.id !== selectedEvent.id));
// setShowModal(false);
// setSelectedEvent(null);
// };
// // Custom components for the calendar
// const components = {
// toolbar: CustomToolbar as any,
// event: CustomEvent as any
// };
// return (
// <div className="relative h-screen px-6">
// <div className="text-5xl mt-10 font-bold">
// Schedule
// </div>
// <div className="border-b my-5 pb-2 border-gray-300">
// <p className="text-gray-500">Plan and schedule your ideas</p>
// </div>
// <div className="h-5/6 pb-10">
// <Calendar
// localizer={localizer}
// events={events}
// startAccessor="start"
// endAccessor="end"
// style={{ height: "100%" }}
// onSelectSlot={handleSelectSlot}
// onSelectEvent={handleSelectEvent}
// selectable
// components={components}
// />
// </div>
// {/* Modal for creating/editing events */}
// {showModal && (
// <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
// <div className="bg-white rounded-lg p-6 w-full max-w-md">
// <div className="flex justify-between items-center mb-4">
// <h2 className="text-xl font-semibold">
// {selectedEvent ? "Event Details" : "Schedule an Idea"}
// </h2>
// <button
// onClick={() => setShowModal(false)}
// className="p-1 hover:bg-gray-100 rounded-full"
// >
// <X size={20} />
// </button>
// </div>
// {selectedEvent ? (
// // View/Edit existing event
// <div>
// <div className="mb-4">
// <h3 className="font-medium text-lg">{selectedEvent.title}</h3>
// {selectedIdea && (
// <div className="mt-2 text-gray-600">
// <p>{getPlainTextFromNodes(selectedIdea.content).substring(0, 100)}...</p>
// {selectedIdea.tags && selectedIdea.tags.length > 0 && (
// <div className="flex gap-1 mt-2 flex-wrap">
// {selectedIdea.tags.map(tag => (
// <span key={tag} className="bg-gray-200 text-gray-700 px-2 py-1 rounded-md text-xs">
// {tag}
// </span>
// ))}
// </div>
// )}
// </div>
// )}
// <div className="flex items-center gap-2 mt-4 text-gray-600">
// <CalendarIcon size={16} />
// <span>
// {moment(selectedEvent.start).format("MMM D, YYYY")}
// </span>
// </div>
// <div className="flex items-center gap-2 mt-2 text-gray-600">
// <Clock size={16} />
// <span>
// {selectedEvent.allDay
// ? "All day"
// : `${moment(selectedEvent.start).format("h:mm A")} - ${moment(selectedEvent.end).format("h:mm A")}`}
// </span>
// </div>
// </div>
// <div className="flex justify-end gap-3 mt-6">
// <button
// onClick={handleDeleteEvent}
// className="px-4 py-2 bg-red-100 text-red-600 rounded hover:bg-red-200"
// >
// Delete
// </button>
// <button
// onClick={() => setShowModal(false)}
// className="px-4 py-2 bg-gray-100 rounded hover:bg-gray-200"
// >
// Close
// </button>
// </div>
// </div>
// ) : (
// // Create new event
// <div>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-1">
// Date
// </label>
// <div className="px-3 py-2 border rounded-md bg-gray-50">
// {moment(selectedSlot.start).format("MMMM D, YYYY")}
// </div>
// </div>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-1">
// Time
// </label>
// <div className="px-3 py-2 border rounded-md bg-gray-50">
// {selectedSlot.slots && selectedSlot.slots.length === 1 && selectedSlot.action === "click"
// ? "All day"
// : moment(selectedSlot.start).format("h:mm A")}
// </div>
// </div>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-1">
// Duration (minutes)
// </label>
// <input
// type="number"
// min="15"
// step="15"
// value={duration}
// onChange={(e) => setDuration(Number(e.target.value))}
// className="w-full px-3 py-2 border rounded-md"
// />
// </div>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-1">
// Select Idea
// </label>
// <input
// type="text"
// placeholder="Search ideas..."
// value={searchTerm}
// onChange={(e) => setSearchTerm(e.target.value)}
// className="w-full px-3 py-2 border rounded-md mb-2"
// />
// <div className="max-h-40 overflow-y-auto border rounded-md">
// {filteredIdeas.length > 0 ? (
// filteredIdeas.map(idea => (
// <div
// key={idea.id}
// onClick={() => setSelectedIdea(idea)}
// className={`p-2 cursor-pointer hover:bg-gray-100 ${
// selectedIdea?.id === idea.id ? "bg-blue-50 border-l-4 border-blue-500" : ""
// }`}
// >
// <div className="font-medium">{idea.title}</div>
// <div className="text-sm text-gray-500 truncate">
// {getPlainTextFromNodes(idea.content).substring(0, 60)}...
// </div>
// </div>
// ))
// ) : (
// <div className="p-3 text-center text-gray-500">
// No ideas found. Try a different search term.
// </div>
// )}
// </div>
// </div>
// <div className="flex justify-end gap-3 mt-6">
// <button
// onClick={() => setShowModal(false)}
// className="px-4 py-2 bg-gray-100 rounded hover:bg-gray-200"
// >
// Cancel
// </button>
// <button
// onClick={handleCreateEvent}
// disabled={!selectedIdea}
// className={`px-4 py-2 rounded ${
// selectedIdea
// ? "bg-blue-600 text-white hover:bg-blue-700"
// : "bg-blue-300 text-white cursor-not-allowed"
// }`}
// >
// Schedule
// </button>
// </div>
// </div>
// )}
// </div>
// </div>
// )}
// </div>
// );
// };
// // Custom toolbar component for the calendar
// const CustomToolbar = ({ label, onNavigate, onView }: ToolbarProps) => {
// return (
// <div className="flex justify-between items-center mb-4">
// <div className="flex items-center gap-2">
// <button
// onClick={() => onNavigate("PREV")}
// className="p-1 hover:bg-gray-100 rounded"
// >
// <ChevronLeft />
// </button>
// <button
// onClick={() => onNavigate("TODAY")}
// className="px-3 py-1 text-sm hover:bg-gray-100 rounded"
// >
// Today
// </button>
// <button
// onClick={() => onNavigate("NEXT")}
// className="p-1 hover:bg-gray-100 rounded"
// >
// <ChevronRight />
// </button>
// <span className="font-semibold text-lg ml-2">{label}</span>
// </div>
// <div className="flex gap-2">
// <button
// onClick={() => onView("month")}
// className="px-3 py-1 text-sm hover:bg-gray-100 rounded"
// >
// Month
// </button>
// <button
// onClick={() => onView("week")}
// className="px-3 py-1 text-sm hover:bg-gray-100 rounded"
// >
// Week
// </button>
// <button
// onClick={() => onView("day")}
// className="px-3 py-1 text-sm hover:bg-gray-100 rounded"
// >
// Day
// </button>
// <button
// onClick={() => onView("agenda")}
// className="px-3 py-1 text-sm hover:bg-gray-100 rounded"
// >
// Agenda
// </button>
// </div>
// </div>
// );
// };
// // Custom event component to style events in the calendar
// interface EventProps {
// event: {
// title: string;
// start: Date;
// end: Date;
// ideaId: string;
// id: string;
// allDay?: boolean;
// };
// }
// const CustomEvent = ({ event }: EventProps) => {
// return (
// <div className="bg-blue-100 border-l-2 border-blue-500 p-1 overflow-hidden text-blue-800">
// <div className="font-medium text-sm truncate">{event.title}</div>
// </div>
// );
// };
// export default Schedule;
import React, { useState, useEffect, useRef } from 'react';
import {
EventApi,
DateSelectArg,
EventClickArg,
EventContentArg,
formatDate,
} from '@fullcalendar/core';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import { useEvents, CalendarEvent } from './../../context/EventsContext';
import EventModal from './../../components/ui/EventModal';
interface SchedulerProps {
userId: number;
}
const Scheduler = () => {
const [weekendsVisible, setWeekendsVisible] = useState(true);
const [currentEvents, setCurrentEvents] = useState<EventApi[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
const [selectedDates, setSelectedDates] = useState<DateSelectArg | null>(null);
// Use the EventsContext instead of direct API calls
const {
events,
loading,
error,
fetchEvents,
addEvent,
updateEvent,
deleteEvent,
getEventsForFullCalendar
} = useEvents();
const calendarRef = useRef<FullCalendar>(null);
// Fetch events from context on component mount
useEffect(() => {
fetchEvents();
}, []);
// Update calendar when events change in context
useEffect(() => {
if (calendarRef.current && events.length > 0) {
const calendarApi = calendarRef.current.getApi();
calendarApi.removeAllEvents();
// Add events from context to the calendar
const fullCalendarEvents = getEventsForFullCalendar();
fullCalendarEvents.forEach(event => {
calendarApi.addEvent(event);
});
}
}, [events]);
const handleWeekendsToggle = () => {
setWeekendsVisible(!weekendsVisible);
};
const handleDateSelect = (selectInfo: DateSelectArg) => {
setSelectedDates(selectInfo);
setSelectedEvent(null);
setIsModalOpen(true);
};
const handleEventClick = (clickInfo: EventClickArg) => {
// Find the corresponding event from our context
const eventId = clickInfo.event.id;
const contextEvent = events.find(event => event.id === eventId);
if (contextEvent) {
setSelectedEvent(contextEvent);
setSelectedDates(null);
setIsModalOpen(true);
}
};
const handleEvents = (events: EventApi[]) => {
setCurrentEvents(events);
};
const handleSaveEvent = async (eventData: Partial<CalendarEvent>) => {
try {
if (selectedEvent) {
// Update existing event using context
// Ensure dates are properly formatted for the API
const updatedEvent: CalendarEvent = {
...selectedEvent,
title: eventData.title || selectedEvent.title,
start: formatDateForAPI(eventData.start) || formatDateForAPI(selectedEvent.start),
end: eventData.end ? formatDateForAPI(eventData.end) : undefined,
allDay: eventData.allDay ?? selectedEvent.allDay,
description: eventData.description,
color: eventData.color
};
const success = await updateEvent(updatedEvent);
if (!success) {
console.error("Failed to update event through context");
}
} else if (selectedDates) {
const userString = JSON.parse(localStorage.getItem("user"))
// Create new event using context
// For new events from date selection, ensure we format the dates properly
const newEvent: Omit<CalendarEvent, 'id' | "userId"> = {
title: eventData.title || 'New Event',
start: formatDateForAPI(eventData.start) || selectedDates.startStr,
end: eventData.end ? formatDateForAPI(eventData.end) : selectedDates.endStr,
allDay: eventData.allDay ?? selectedDates.allDay,
description: eventData.description,
color: eventData.color,
// userId: userString.id
};
console.log('Creating new event with data:', newEvent);
const createdEvent = await addEvent(newEvent);
if (!createdEvent) {
console.error("Failed to create event through context");
}
}
} catch (err) {
console.error('Error saving event:', err);
}
// Close the modal and reset selection
setIsModalOpen(false);
setSelectedEvent(null);
setSelectedDates(null);
};
// Helper function to ensure consistent date formatting for API calls
const formatDateForAPI = (date: Date | string | undefined): string => {
if (!date) return '';
if (typeof date === 'string') {
// If already a string, ensure it's in ISO format
// Try to parse and convert to ensure valid format
try {
return new Date(date).toISOString();
} catch (e) {
console.error('Invalid date string:', date);
return '';
}
}
// If it's a Date object, convert to ISO string
return date.toISOString();
};
const handleDeleteEvent = async () => {
if (!selectedEvent) return;
try {
const success = await deleteEvent(selectedEvent.id);
if (!success) {
console.error("Failed to delete event through context");
}
} catch (err) {
console.error('Error deleting event:', err);
}
// Close the modal and reset selection
setIsModalOpen(false);
setSelectedEvent(null);
setSelectedDates(null);
};
const renderEventContent = (eventContent: EventContentArg) => {
return (
<>
<b>{eventContent.timeText}</b>
<i>{eventContent.event.title}</i>
</>
);
};
const renderSidebarEvent = (event: EventApi) => {
return (
<li key={event.id} className="p-2 mb-1 bg-gray-100 rounded">
<b>{formatDate(event.start!, { year: 'numeric', month: 'short', day: 'numeric' })}</b>
<i className="ml-2">{event.title}</i>
</li>
);
};
return (
<div className="flex flex-col lg:flex-row h-full">
<div className="w-full lg:w-64 p-4 bg-white border-r">
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">Calendar Settings</h2>
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={weekendsVisible}
onChange={handleWeekendsToggle}
className="mr-2"
/>
<span>Show weekends</span>
</label>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">
Events ({currentEvents.length})
</h2>
{loading ? (
<p>Loading events...</p>
) : error ? (
<p className="text-red-500">{error}</p>
) : (
<ul className="space-y-1 max-h-96 overflow-y-auto">
{currentEvents.map(renderSidebarEvent)}
</ul>
)}
</div>
</div>
<div className="flex-1 p-4">
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
}}
initialView="dayGridMonth"
editable={true}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
weekends={weekendsVisible}
select={handleDateSelect}
eventContent={renderEventContent}
eventClick={handleEventClick}
eventsSet={handleEvents}
height="auto"
/>
</div>
<EventModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedEvent(null);
setSelectedDates(null);
}}
onSave={handleSaveEvent}
onDelete={handleDeleteEvent}
selectedEvent={selectedEvent}
selectedDates={selectedDates}
/>
</div>
);
};
export default Scheduler;

View File

@ -0,0 +1,44 @@
export interface User {
id: number;
email: string;
name: string;
password: string;
createdAt: string;
}
export interface AuthState {
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
}
// Idea types
export interface Idea {
id: string;
userId: string;
title: string;
description: string;
category: string;
status: IdeaStatus;
progress: number;
createdAt: string;
updatedAt: string;
}
export type IdeaStatus = 'ideation' | 'drafting' | 'in-progress' | 'finished';
// Content types
export interface Content {
id: string;
ideaId: string;
type: ContentType;
title: string;
data: string;
createdAt: string;
updatedAt: string;
}
export type ContentType = 'text' | 'image' | 'video';
// Theme types
export type Theme = 'light' | 'dark';

View File

@ -0,0 +1,95 @@
// import { CalendarEvent } from '../context/EventsContext';
// // Base API URL - replace with your actual API endpoint
// const API_URL = '/api';
// // Helper function to get the auth token
// const getAuthToken = (): string | null => {
// const user = localStorage.getItem('user');
// if (!user) return null;
// try {
// // This assumes your token is stored in the user object
// // Adjust according to your actual token storage method
// const userData = JSON.parse(user);
// return userData.token || null;
// } catch (error) {
// console.error('Error parsing user data:', error);
// return null;
// }
// };
// // Generic fetch helper with authentication
// async function fetchWithAuth(url: string, options: RequestInit = {}) {
// const token = getAuthToken();
// const headers = {
// 'Content-Type': 'application/json',
// ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
// ...options.headers
// };
// const response = await fetch(`${API_URL}${url}`, {
// ...options,
// headers
// });
// if (!response.ok) {
// const error = await response.json().catch(() => ({}));
// throw new Error(error.message || `API request failed with status ${response.status}`);
// }
// return response.json();
// }
// // Event service functions
// export const EventService = {
// // Get all events for a user
// getUserEvents: async (userId: number): Promise<CalendarEvent[]> => {
// return fetchWithAuth(`/events/user/${userId}`);
// },
// // Create a new event
// createEvent: async (event: Omit<CalendarEvent, 'id'>): Promise<CalendarEvent> => {
// return fetchWithAuth('/events', {
// method: 'POST',
// body: JSON.stringify(event)
// });
// },
// // Update an existing event
// updateEvent: async (event: CalendarEvent): Promise<CalendarEvent> => {
// return fetchWithAuth(`/events/${event.id}`, {
// method: 'PUT',
// body: JSON.stringify(event)
// });
// },
// // Delete an event
// deleteEvent: async (eventId: string): Promise<void> => {
// return fetchWithAuth(`/events/${eventId}`, {
// method: 'DELETE'
// });
// },
// // Get events for a specific idea
// getEventsByIdeaId: async (ideaId: string): Promise<CalendarEvent[]> => {
// return fetchWithAuth(`/events/idea/${ideaId}`);
// },
// // Link an event to an idea
// linkEventToIdea: async (eventId: string, ideaId: string): Promise<void> => {
// return fetchWithAuth(`/events/${eventId}/link/${ideaId}`, {
// method: 'POST'
// });
// },
// // Unlink an event from an idea
// unlinkEventFromIdea: async (eventId: string, ideaId: string): Promise<void> => {
// return fetchWithAuth(`/events/${eventId}/unlink/${ideaId}`, {
// method: 'DELETE'
// });
// }
// };
// export default EventService;

1
apps/project1/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,113 @@
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f6ff',
100: '#e0edff',
200: '#c0daff',
300: '#92bfff',
400: '#5c9aff',
500: '#3374ff',
600: '#1e55ff',
700: '#1342f1',
800: '#1635d1',
900: '#1733a5',
950: '#101f5e',
},
secondary: {
50: '#f5f3ff',
100: '#ede8ff',
200: '#ded5ff',
300: '#c6b4ff',
400: '#aa88ff',
500: '#9055ff',
600: '#7c38f2',
700: '#6a24db',
800: '#5820b5',
900: '#481c91',
950: '#2c0e67',
},
success: {
50: '#edfdf4',
100: '#d3fadf',
200: '#aaf0c4',
300: '#73e2a3',
400: '#3acd7e',
500: '#16b364',
600: '#098f4e',
700: '#087241',
800: '#095c36',
900: '#084c2e',
950: '#022b1a',
},
warning: {
50: '#fffce8',
100: '#fff9c2',
200: '#fff087',
300: '#ffdf41',
400: '#ffcb0a',
500: '#edb100',
600: '#cc8a00',
700: '#a26304',
800: '#874e0c',
900: '#734010',
950: '#422105',
},
error: {
50: '#fff1f2',
100: '#ffe4e5',
200: '#ffccce',
300: '#ffa2a6',
400: '#fd6d72',
500: '#f53e44',
600: '#e12028',
700: '#bd161e',
800: '#9d151c',
900: '#82151c',
950: '#470709',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'scale-up': 'scaleUp 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: 0 },
'100%': { transform: 'translateY(0)', opacity: 1 },
},
slideDown: {
'0%': { transform: 'translateY(-10px)', opacity: 0 },
'100%': { transform: 'translateY(0)', opacity: 1 },
},
scaleUp: {
'0%': { transform: 'scale(0.95)', opacity: 0 },
'100%': { transform: 'scale(1)', opacity: 1 },
},
},
},
},
plugins: [],
};

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss()
],
})

36
apps/web/.gitignore vendored
View File

@ -1,36 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,50 +0,0 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
.imgDark {
display: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
.imgLight {
display: none;
}
.imgDark {
display: unset;
}
}

View File

@ -1,31 +0,0 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}

View File

@ -1,188 +0,0 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-synthesis: none;
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 180px;
}
button.secondary {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
background: transparent;
border-color: var(--gray-alpha-200);
min-width: 180px;
}
.footer {
font-family: var(--font-geist-sans);
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}

View File

@ -1,102 +0,0 @@
import Image, { type ImageProps } from "next/image";
import { Button } from "@repo/ui/button";
import styles from "./page.module.css";
type Props = Omit<ImageProps, "src"> & {
srcLight: string;
srcDark: string;
};
const ThemeImage = (props: Props) => {
const { srcLight, srcDark, ...rest } = props;
return (
<>
<Image {...rest} src={srcLight} className="imgLight" />
<Image {...rest} src={srcDark} className="imgDark" />
</>
);
};
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<ThemeImage
className={styles.logo}
srcLight="turborepo-dark.svg"
srcDark="turborepo-light.svg"
alt="Turborepo logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>apps/web/app/page.tsx</code>
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new/clone?demo-description=Learn+to+implement+a+monorepo+with+a+two+Next.js+sites+that+has+installed+three+local+packages.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4K8ZISWAzJ8X1504ca0zmC%2F0b21a1c6246add355e55816278ef54bc%2FBasic.png&demo-title=Monorepo+with+Turborepo&demo-url=https%3A%2F%2Fexamples-basic-web.vercel.sh%2F&from=templates&project-name=Monorepo+with+Turborepo&repository-name=monorepo-turborepo&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fturborepo%2Ftree%2Fmain%2Fexamples%2Fbasic&root-directory=apps%2Fdocs&skippable-integrations=1&teamSlug=vercel&utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://turborepo.com/docs?utm_source"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
<Button appName="web" className={styles.secondary}>
Open alert
</Button>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com/templates?search=turborepo&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://turborepo.com?utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to turborepo.com
</a>
</footer>
</div>
);
}

View File

@ -1,4 +0,0 @@
import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config} */
export default nextJsConfig;

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View File

@ -1,28 +0,0 @@
{
"name": "web",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3000",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@repo/ui": "*",
"next": "^15.3.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.26.0",
"typescript": "5.8.2"
}
}

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 13.5V6.5V5.41421C14.5 5.149 14.3946 4.89464 14.2071 4.70711L9.79289 0.292893C9.60536 0.105357 9.351 0 9.08579 0H8H3H1.5V1.5V13.5C1.5 14.8807 2.61929 16 4 16H12C13.3807 16 14.5 14.8807 14.5 13.5ZM13 13.5V6.5H9.5H8V5V1.5H3V13.5C3 14.0523 3.44772 14.5 4 14.5H12C12.5523 14.5 13 14.0523 13 13.5ZM9.5 5V2.12132L12.3787 5H9.5ZM5.13 5.00062H4.505V6.25062H5.13H6H6.625V5.00062H6H5.13ZM4.505 8H5.13H11H11.625V9.25H11H5.13H4.505V8ZM5.13 11H4.505V12.25H5.13H11H11.625V11H11H5.13Z" fill="#666666"/>
</svg>

Before

Width:  |  Height:  |  Size: 645 B

View File

@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_868_525)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z" fill="#666666"/>
</g>
<defs>
<clipPath id="clip0_868_525">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,19 +0,0 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6565V22.3773H91.0977V30.6565H106.16V58.1875H115.935V30.6565H130.998Z" fill="black"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2275V22.3773H162.768V41.2799C162.768 47.0155 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0155 144.315 41.2799V22.3773H134.539V42.2275C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="black"/>
<path d="M187.508 46.3173H197.234L204.914 58.1875H216.136L207.458 45.2699C212.346 43.5243 215.338 39.634 215.338 34.3473C215.338 26.6665 209.603 22.3773 200.874 22.3773H177.732V58.1875H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.053 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="black"/>
<path d="M219.887 58.1875H245.472C253.452 58.1875 258.041 54.397 258.041 48.0629C258.041 43.8235 255.348 40.9308 252.156 39.634C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1875ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0205 248.615 46.9657C248.615 48.9108 247.168 50.2075 244.525 50.2075H229.263V43.7238Z" fill="black"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.786 281.942 58.786C294.461 58.786 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2575C288.525 30.2575 293.463 34.1478 293.463 40.2824C293.463 46.417 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.417 270.421 40.2824C270.421 34.1478 275.359 30.2575 281.942 30.2575Z" fill="black"/>
<path d="M317.526 46.3173H327.251L334.932 58.1875H346.154L337.476 45.2699C342.364 43.5243 345.356 39.634 345.356 34.3473C345.356 26.6665 339.62 22.3773 330.892 22.3773H307.75V58.1875H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.053 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="black"/>
<path d="M349.904 22.3773V58.1875H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6565H384.717V22.3773H349.904Z" fill="black"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1875H399.204V46.7662ZM399.204 38.6365V30.5568H411.673C415.164 30.5568 417.059 32.053 417.059 34.5967C417.059 37.0904 415.164 38.6365 411.673 38.6365H399.204Z" fill="black"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.786 450.948 58.786C463.467 58.786 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2575C457.532 30.2575 462.469 34.1478 462.469 40.2824C462.469 46.417 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.417 439.427 40.2824C439.427 34.1478 444.365 30.2575 450.948 30.2575Z" fill="black"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_278)"/>
<defs>
<linearGradient id="paint0_linear_2028_278" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,19 +0,0 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6566V22.3773H91.0977V30.6566H106.16V58.1876H115.935V30.6566H130.998Z" fill="white"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2276V22.3773H162.768V41.2799C162.768 47.0156 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0156 144.315 41.2799V22.3773H134.539V42.2276C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="white"/>
<path d="M187.508 46.3173H197.234L204.914 58.1876H216.136L207.458 45.2699C212.346 43.5243 215.338 39.6341 215.338 34.3473C215.338 26.6666 209.603 22.3773 200.874 22.3773H177.732V58.1876H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.0531 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="white"/>
<path d="M219.887 58.1876H245.472C253.452 58.1876 258.041 54.3971 258.041 48.0629C258.041 43.8236 255.348 40.9308 252.156 39.6341C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1876ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0206 248.615 46.9657C248.615 48.9108 247.168 50.2076 244.525 50.2076H229.263V43.7238Z" fill="white"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.7861 281.942 58.7861C294.461 58.7861 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2576C288.525 30.2576 293.463 34.1478 293.463 40.2824C293.463 46.4171 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.4171 270.421 40.2824C270.421 34.1478 275.359 30.2576 281.942 30.2576Z" fill="white"/>
<path d="M317.526 46.3173H327.251L334.932 58.1876H346.154L337.476 45.2699C342.364 43.5243 345.356 39.6341 345.356 34.3473C345.356 26.6666 339.62 22.3773 330.892 22.3773H307.75V58.1876H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.0531 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="white"/>
<path d="M349.904 22.3773V58.1876H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6566H384.717V22.3773H349.904Z" fill="white"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1876H399.204V46.7662ZM399.204 38.6366V30.5568H411.673C415.164 30.5568 417.059 32.0531 417.059 34.5967C417.059 37.0904 415.164 38.6366 411.673 38.6366H399.204Z" fill="white"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.7861 450.948 58.7861C463.467 58.7861 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2576C457.532 30.2576 462.469 34.1478 462.469 40.2824C462.469 46.4171 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.4171 439.427 40.2824C439.427 34.1478 444.365 30.2576 450.948 30.2576Z" fill="white"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_477)"/>
<defs>
<linearGradient id="paint0_linear_2028_477" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,10 +0,0 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_977_547)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3L18.5 17H2.5L10.5 3Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_977_547">
<rect width="16" height="16" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 367 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM3.75 5.5C4.16421 5.5 4.5 5.16421 4.5 4.75C4.5 4.33579 4.16421 4 3.75 4C3.33579 4 3 4.33579 3 4.75C3 5.16421 3.33579 5.5 3.75 5.5ZM7 4.75C7 5.16421 6.66421 5.5 6.25 5.5C5.83579 5.5 5.5 5.16421 5.5 4.75C5.5 4.33579 5.83579 4 6.25 4C6.66421 4 7 4.33579 7 4.75ZM8.75 5.5C9.16421 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.16421 4 8.75 4C8.33579 4 8 4.33579 8 4.75C8 5.16421 8.33579 5.5 8.75 5.5Z" fill="#666666"/>
</svg>

Before

Width:  |  Height:  |  Size: 750 B

View File

@ -1,20 +0,0 @@
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
"next.config.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

13806
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More