Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Discord webhook URL for contact form notifications
# Create a webhook in your Discord server and paste the URL here
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your-webhook-url

# Optional: Discord role ID to mention in contact form notifications
# Enable Developer Mode in Discord, right-click on the role and select "Copy ID"
DISCORD_NOTIFICATION_ROLE_ID=123456789012345678
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ yarn-error.log*
.env*.local
.env
.env.*
!.env.example

# vercel
.vercel
Expand Down
113 changes: 55 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,85 @@
# QuotesAI
# Soul Solidity Website

QuotesAI is built using Next.js, Tailwind CSS, Shadcn-ui, Magic-ui, Supabase, NextAuth, and Prisma. It is powered by Vercel and the OpenAI API. It uses the Goodreads API to generate category-based quotes as per your current mood/vibe.
Soul Solidity is a developer lab with a passion for Solidity. We build simple, secure, and robust decentralized systems. Our focus on innovation, transparency, and efficiency delivers trusted solutions for the blockchain ecosystem.

## Video Overview
## Technology Stack

Watch the video below for a quick overview of QuotesAI:
- **Frontend**: Next.js 13, React 18, TailwindCSS
- **UI Components**: Radix UI, Framer Motion
- **Styling**: TailwindCSS with custom components
- **State Management**: React hooks and context
- **API**: Next.js API routes

https://github.com/DarkInventor/QuotesAI/assets/67015517/e59b2402-772b-4ede-a28d-951278e6c555
## Features


## Environment Variables

### Supabase Connection Pooling

```
DATABASE_URL=
```

### NextAuth Configuration

```
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
```

### Google OAuth Configuration

```
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```

### GitHub OAuth Configuration

```
GITHUB_ID=
GITHUB_SECRET=
GITHUB_ACCESS_TOKEN=
```

### Stripe Configuration

```
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
```
- Responsive design
- Dark/light mode support
- Product showcase
- Contact form with Discord integration
- Social proof section
- Company statistics

## Setup Instructions

1. **Clone the repository:**

```sh
git clone https://github.com/DarkInventor/QuotesAI.git
cd QuotesAI
git clone https://github.com/SoulSolidity/soul-solidity-website.git
cd soul-solidity-website
```

2. **Create and populate the `.env` file:**
2. **Install dependencies:**

```sh
cp .env.example .env
yarn install
```
Edit the `.env` file and add your credentials.

3. **Install dependencies:**
3. **Create and populate the `.env.local` file:**

```sh
pnpm install
cp .env.local.example .env.local
```

Edit the `.env.local` file and add your credentials.

4. **Run the development server:**

```sh
pnpm run dev
yarn dev
```

5. **Open your browser and navigate to:**

```
http://localhost:3000
```

## License
## Environment Variables

### Discord Integration

This project is licensed under the MIT License. See the [LICENSE](https://github.com/DarkInventor/QuotesAI/blob/main/License.md) file for details.
```
# Discord webhook URL for contact form notifications
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your-webhook-url

# Optional: Discord role ID to mention in contact form notifications
DISCORD_NOTIFICATION_ROLE_ID=123456789012345678
```

See the [Discord Webhook Integration Guide](./docs/integration/discord-webhook.md) for detailed setup instructions.

## Documentation

## Contributing
- [Architecture Overview](./docs/architecture/overview.md)
- [Architecture Decision Records](./docs/adr/)
- [Integration Guides](./docs/integration/)

## Contact Form Integration

The contact form uses a dependency injection pattern to allow for multiple notification channels. Currently, it supports sending notifications to Discord via webhooks, but it can be extended to support other channels like email.

See the [Contact Service Architecture Diagram](./docs/architecture/diagrams/contact-service.md) for more details.

## License

1. Fork the repository.
2. Create your feature branch (`git checkout -b feature/your-feature`).
3. Commit your changes (`git commit -am 'Add some feature'`).
4. Push to the branch (`git push origin feature/your-feature`).
5. Create a new Pull Request.
This project is licensed under the MIT License. See the [LICENSE](./License.md) file for details.
143 changes: 143 additions & 0 deletions app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { ContactServiceFactory } from "@/app/services/contact/ContactServiceFactory";
import { ContactFormData } from "@/app/services/contact/IContactService";
import { NextResponse } from "next/server";

// Simple rate limiting
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 5;
const ipRequestCounts = new Map<string, { count: number; resetTime: number }>();

/**
* Rate limiting middleware
*
* @param ip The IP address to check
* @returns true if the request should be rate limited, false otherwise
*/
function shouldRateLimit(ip: string): boolean {
const now = Date.now();
const record = ipRequestCounts.get(ip);

// If no record exists or the reset time has passed, create a new record
if (!record || now > record.resetTime) {
ipRequestCounts.set(ip, {
count: 1,
resetTime: now + RATE_LIMIT_WINDOW,
});
return false;
}

// Increment the count
record.count += 1;

// Check if the count exceeds the limit
if (record.count > MAX_REQUESTS_PER_WINDOW) {
return true;
}

return false;
}

/**
* Validate contact form data
*
* @param data The contact form data to validate
* @returns An object with validation errors, or null if the data is valid
*/
function validateContactForm(data: any): Record<string, string> | null {
const errors: Record<string, string> = {};

// Check if required fields are present
if (!data) {
errors.general = "No data provided";
return errors;
}

// Validate name
if (
!data.name ||
typeof data.name !== "string" ||
data.name.trim().length < 2
) {
errors.name = "Name must be at least 2 characters";
}

// Validate email
if (
!data.email ||
typeof data.email !== "string" ||
!data.email.includes("@")
) {
errors.email = "Please enter a valid email";
}

// Validate message
if (
!data.message ||
typeof data.message !== "string" ||
data.message.trim().length < 10
) {
errors.message = "Message must be at least 10 characters";
}

return Object.keys(errors).length > 0 ? errors : null;
}

/**
* POST handler for contact form submissions
*/
export async function POST(request: Request) {
try {
// Get client IP for rate limiting
// In a real production environment, you would get this from headers like X-Forwarded-For
const ip = request.headers.get("x-forwarded-for") || "unknown";

// Check rate limiting
if (shouldRateLimit(ip)) {
return NextResponse.json(
{ success: false, error: "Too many requests. Please try again later." },
{ status: 429 }
);
}

// Parse request body
const data = await request.json();

// Validate form data
const validationErrors = validateContactForm(data);
if (validationErrors) {
return NextResponse.json(
{ success: false, errors: validationErrors },
{ status: 400 }
);
}

// Get contact service
const contactService = ContactServiceFactory.getContactService();

// Send contact form data
const success = await contactService.sendContactForm(
data as ContactFormData
);

if (success) {
return NextResponse.json({ success: true });
} else {
return NextResponse.json(
{
success: false,
error: "Failed to send message. Please try again later.",
},
{ status: 500 }
);
}
} catch (error) {
console.error("Error in contact API route:", error);
return NextResponse.json(
{
success: false,
error: "An unexpected error occurred. Please try again later.",
},
{ status: 500 }
);
}
}
55 changes: 49 additions & 6 deletions app/components/contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import { Icons } from "@/components/icons";
import { Mail } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useState, FormEvent } from "react";
import { useToast } from "@/hooks/use-toast";

interface SocialLink {
href: string;
Expand Down Expand Up @@ -44,16 +44,59 @@ const Contact = () => {
return Object.keys(newErrors).length === 0;
};

const { toast } = useToast();

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) return;

setIsSubmitting(true);
// Here you would typically send the form data to your backend
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
setIsSubmitting(false);
setFormData({ name: "", email: "", message: "" });
// Add toast notification here

try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});

const data = await response.json();

if (data.success) {
// Success
toast({
title: "Message sent!",
description: "We'll get back to you as soon as possible.",
variant: "default",
});
// Reset form
setFormData({ name: "", email: "", message: "" });
} else {
// API returned an error
if (data.errors) {
// Validation errors
setErrors(data.errors);
} else {
// General error
toast({
title: "Error",
description: data.error || "Something went wrong. Please try again.",
variant: "destructive",
});
}
}
} catch (error) {
// Network or other error
toast({
title: "Error",
description: "Could not connect to the server. Please try again later.",
variant: "destructive",
});
console.error("Contact form error:", error);
} finally {
setIsSubmitting(false);
}
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
Expand Down
Loading