mvp
85
README.md
@ -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
@ -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
@ -0,0 +1,2 @@
|
||||
const JWT_SECRET = "creatorHub_jwt_secret"
|
||||
export default JWT_SECRET
|
81
apps/backend/constant.ts
Normal 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
@ -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
@ -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
|
12
apps/backend/mainRouter.ts
Normal 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
|
69
apps/backend/middleware.ts
Normal 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
@ -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
@ -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
@ -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)
|
8
apps/backend/tsconfig.json
Normal 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
@ -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
|
@ -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.
|
Before Width: | Height: | Size: 25 KiB |
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default nextJsConfig;
|
@ -1,4 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
@ -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"
|
||||
}
|
||||
}
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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
@ -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
@ -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,
|
||||
},
|
||||
})
|
||||
```
|
28
apps/project1/eslint.config.js
Normal 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
@ -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
67
apps/project1/package.json
Normal 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"
|
||||
}
|
||||
}
|
2
apps/project1/public/sparkles-svgrepo-com.svg
Normal 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 |
1
apps/project1/public/vite.svg
Normal 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
@ -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
@ -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
|
1
apps/project1/src/assets/react.svg
Normal 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 |
54
apps/project1/src/calendar.css
Normal 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;
|
||||
}
|
408
apps/project1/src/components/ui/BlockEditor.tsx
Normal 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;
|
64
apps/project1/src/components/ui/Button.tsx
Normal 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;
|
54
apps/project1/src/components/ui/Card.tsx
Normal 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>
|
||||
);
|
||||
};
|
1297
apps/project1/src/components/ui/CreateIdea.tsx
Normal file
200
apps/project1/src/components/ui/EventModal.tsx
Normal 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"
|
||||
>
|
||||
×
|
||||
</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;
|
40
apps/project1/src/components/ui/Features.tsx
Normal 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>
|
||||
}
|
17
apps/project1/src/components/ui/HeroSection1.tsx
Normal 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>
|
||||
}
|
176
apps/project1/src/components/ui/IdeaCard.tsx
Normal 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;
|
77
apps/project1/src/components/ui/Input.tsx
Normal 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;
|
340
apps/project1/src/components/ui/Platform.tsx
Normal 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;
|
468
apps/project1/src/components/ui/RichTextEditor.tsx
Normal 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;
|
59
apps/project1/src/components/ui/Textarea.tsx
Normal 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;
|
315
apps/project1/src/context/AIContentStream.ts
Normal 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: '' }]
|
||||
}];
|
||||
};
|
146
apps/project1/src/context/AuthContext.tsx
Normal 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;
|
||||
};
|
345
apps/project1/src/context/EventsContext.tsx
Normal 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;
|
||||
};
|
244
apps/project1/src/context/IdeasContext.tsx
Normal 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;
|
||||
};
|
57
apps/project1/src/context/ThemeContext.tsx
Normal 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;
|
||||
};
|
85
apps/project1/src/index.css
Normal 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;
|
||||
}
|
||||
} */
|
24
apps/project1/src/layout/Layout.tsx
Normal 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">
|
||||
© {new Date().getFullYear()} CreatorHub. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
38
apps/project1/src/layout/ProtectedRoute.tsx
Normal 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;
|
21
apps/project1/src/layout/Topbar.tsx
Normal 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>
|
||||
}
|
10
apps/project1/src/main.tsx
Normal 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>,
|
||||
)
|
20
apps/project1/src/pages/Landing.tsx
Normal 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> */}
|
111
apps/project1/src/pages/auth/Login.tsx
Normal 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;
|
135
apps/project1/src/pages/auth/Register.tsx
Normal 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;
|
120
apps/project1/src/pages/ideas/Ideas.tsx
Normal 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;
|
672
apps/project1/src/pages/schedule/Scheduler.tsx
Normal 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;
|
44
apps/project1/src/types/index.ts
Normal 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';
|
95
apps/project1/src/utils/EventService.tsx
Normal 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
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
113
apps/project1/tailwind.config.js
Normal 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: [],
|
||||
};
|
26
apps/project1/tsconfig.app.json
Normal 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"]
|
||||
}
|
7
apps/project1/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
24
apps/project1/tsconfig.node.json
Normal 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"]
|
||||
}
|
11
apps/project1/vite.config.ts
Normal 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
@ -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
|
@ -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.
|
Before Width: | Height: | Size: 25 KiB |
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default nextJsConfig;
|
@ -1,4 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
@ -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"
|
||||
}
|
||||
}
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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"
|
||||
]
|
||||
}
|