diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..a78e57819 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 00a11618b..477bdd159 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* .env*.local .env .env.* +!.env.example # vercel .vercel diff --git a/README.md b/README.md index f2112a0aa..70d6cb283 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +This project is licensed under the MIT License. See the [LICENSE](./License.md) file for details. diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts new file mode 100644 index 000000000..160330204 --- /dev/null +++ b/app/api/contact/route.ts @@ -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(); + +/** + * 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 | null { + const errors: Record = {}; + + // 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 } + ); + } +} diff --git a/app/components/contact.tsx b/app/components/contact.tsx index 4bf68a2b3..43bf4710f 100644 --- a/app/components/contact.tsx +++ b/app/components/contact.tsx @@ -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; @@ -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) => { diff --git a/app/services/contact/ContactServiceFactory.ts b/app/services/contact/ContactServiceFactory.ts new file mode 100644 index 000000000..1f69f78b7 --- /dev/null +++ b/app/services/contact/ContactServiceFactory.ts @@ -0,0 +1,49 @@ +import { IContactService } from "./IContactService"; +import { DiscordContactService } from "./DiscordContactService"; + +/** + * Factory for creating contact services + * + * This factory creates the appropriate implementation of the IContactService + * interface based on configuration. + */ +export class ContactServiceFactory { + /** + * Get the appropriate contact service implementation + * + * Currently, this returns a Discord contact service, but in the future + * it could be extended to support other service types based on configuration. + * + * @returns An implementation of the IContactService interface + */ + static getContactService(): IContactService { + // Check if Discord webhook URL is configured + const webhookUrl = process.env.DISCORD_WEBHOOK_URL; + if (!webhookUrl) { + throw new Error("DISCORD_WEBHOOK_URL environment variable is not set"); + } + + // Get optional role ID for mentions + const roleId = process.env.DISCORD_NOTIFICATION_ROLE_ID; + + // Return Discord contact service + return new DiscordContactService(webhookUrl, roleId); + } + + /** + * Get a mock contact service for testing + * + * This method is useful for testing components that depend on the contact service + * without actually sending messages to external services. + * + * @returns A mock implementation of the IContactService interface + */ + static getMockContactService(): IContactService { + return { + sendContactForm: async () => { + console.log("Mock contact service: message sent"); + return true; + }, + }; + } +} diff --git a/app/services/contact/DiscordContactService.ts b/app/services/contact/DiscordContactService.ts new file mode 100644 index 000000000..9da8c2f17 --- /dev/null +++ b/app/services/contact/DiscordContactService.ts @@ -0,0 +1,75 @@ +import { ContactFormData, IContactService } from "./IContactService"; + +/** + * Discord contact service implementation + * + * This service sends contact form submissions to a Discord channel via webhooks + */ +export class DiscordContactService implements IContactService { + private webhookUrl: string; + private roleId?: string; + + /** + * Create a new Discord contact service + * + * @param webhookUrl The Discord webhook URL + * @param roleId Optional Discord role ID to mention in the message + */ + constructor(webhookUrl: string, roleId?: string) { + if (!webhookUrl) { + throw new Error("Discord webhook URL is required"); + } + this.webhookUrl = webhookUrl; + this.roleId = roleId; + } + + /** + * Send contact form data to Discord + * + * @param data The contact form data to send + * @returns A promise that resolves to true if the message was sent successfully, false otherwise + */ + async sendContactForm(data: ContactFormData): Promise { + try { + // Format message with role mention if available + const roleMention = this.roleId ? `<@&${this.roleId}> ` : ""; + + // Create a formatted message for Discord + const response = await fetch(this.webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: `${roleMention}New contact form submission!`, + embeds: [ + { + title: "Contact Form Submission", + color: 0x00ffff, // Cyan color + fields: [ + { name: "Name", value: data.name, inline: true }, + { name: "Email", value: data.email, inline: true }, + { name: "Message", value: data.message }, + ], + footer: { + text: "Soul Solidity Contact Form", + }, + timestamp: new Date().toISOString(), + }, + ], + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Discord webhook error:", response.status, errorText); + return false; + } + + return true; + } catch (error) { + console.error("Error sending to Discord:", error); + return false; + } + } +} diff --git a/app/services/contact/IContactService.ts b/app/services/contact/IContactService.ts new file mode 100644 index 000000000..6f2133335 --- /dev/null +++ b/app/services/contact/IContactService.ts @@ -0,0 +1,24 @@ +/** + * Interface for contact form data + */ +export interface ContactFormData { + name: string; + email: string; + message: string; +} + +/** + * Interface for contact services + * + * This interface defines the contract for sending contact form data + * to various notification channels (Discord, email, etc.) + */ +export interface IContactService { + /** + * Send contact form data to the notification channel + * + * @param data The contact form data to send + * @returns A promise that resolves to true if the message was sent successfully, false otherwise + */ + sendContactForm(data: ContactFormData): Promise; +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..ab1efcaa3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Soul Solidity Documentation + +This directory contains documentation for the Soul Solidity website and its components. + +## Contents + +- [Architecture](./architecture/): System architecture documentation + - [Diagrams](./architecture/diagrams/): Architecture diagrams +- [ADR](./adr/): Architecture Decision Records +- [Integration](./integration/): Integration guides for external services + +## Architecture Decision Records (ADRs) + +ADRs are used to document significant architectural decisions made during the development of the project. They provide context, rationale, and consequences for each decision. + +## Integration Guides + +Integration guides provide step-by-step instructions for setting up and configuring external services used by the application. diff --git a/docs/adr/0001-contact-service-interface.md b/docs/adr/0001-contact-service-interface.md new file mode 100644 index 000000000..31d2bd629 --- /dev/null +++ b/docs/adr/0001-contact-service-interface.md @@ -0,0 +1,60 @@ +# ADR-0001: Contact Service Interface and Dependency Injection + +## Status + +Accepted + +## Context + +The website includes a contact form that allows users to send messages to the Soul Solidity team. Currently, the form is not connected to any backend service. We need to implement a solution that: + +1. Sends contact form submissions to the team via Discord +2. Is flexible enough to support additional notification channels in the future (e.g., email) +3. Keeps sensitive configuration (like webhook URLs) secure +4. Provides a clean separation of concerns + +## Decision + +We will implement a service interface pattern with dependency injection for the contact form backend: + +1. Create an `IContactService` interface that defines the contract for sending contact form data +2. Implement a concrete `DiscordContactService` that sends notifications to Discord via webhooks +3. Use a factory pattern to create the appropriate service implementation +4. Create a Next.js API route that uses the service to process form submissions + +This approach allows us to: + +- Easily add new notification channels by creating new implementations of the interface +- Keep the API route code clean and focused on request handling +- Test components in isolation by mocking the service interface + +## Consequences + +### Positive + +- Flexibility to add or change notification channels without modifying the core logic +- Clear separation of concerns between the API route and the notification logic +- Improved testability through interface mocking +- Secure handling of sensitive configuration (webhook URLs stored server-side only) + +### Negative + +- Slightly more complex than a direct implementation +- Additional files and abstractions to maintain +- Potential overhead for a simple use case (though the benefits outweigh this for future extensibility) + +## Alternatives Considered + +### Direct Implementation in API Route + +We could implement the Discord webhook logic directly in the API route without an interface or factory pattern. This would be simpler initially but would make it harder to add new notification channels or test the code in isolation. + +### Third-Party Form Service + +We could use a third-party service like Formspree or Netlify Forms to handle form submissions. This would require less code but would introduce an external dependency and potentially limit customization options. + +## References + +- [Dependency Injection Pattern](https://en.wikipedia.org/wiki/Dependency_injection) +- [Discord Webhook API Documentation](https://discord.com/developers/docs/resources/webhook) +- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 000000000..9e05623d0 --- /dev/null +++ b/docs/adr/template.md @@ -0,0 +1,25 @@ +# ADR-XXXX: Title + +## Status + +[Proposed | Accepted | Deprecated | Superseded] + +## Context + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +## Decision + +[Describe the decision that was made and the rationale behind it.] + +## Consequences + +[Describe the resulting context after applying the decision. All consequences should be listed here, not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future.] + +## Alternatives Considered + +[Optional: Describe alternatives that were considered and why they were not chosen.] + +## References + +[Optional: List any references that were helpful in making the decision.] diff --git a/docs/architecture/diagrams/contact-service.md b/docs/architecture/diagrams/contact-service.md new file mode 100644 index 000000000..984815a72 --- /dev/null +++ b/docs/architecture/diagrams/contact-service.md @@ -0,0 +1,38 @@ +# Contact Service Architecture Diagram + +```mermaid +graph TD + A[Contact Form Component] --> B[API Route] + B --> C[Contact Service Factory] + C --> D[IContactService Interface] + D --> E[Discord Contact Service] + D -.-> F[Email Contact Service] + F -.-> G[Future Implementations] + + E --> H[Discord Webhook] + F -.-> I[Email Service] + + style F stroke-dasharray: 5 5 + style G stroke-dasharray: 5 5 + style I stroke-dasharray: 5 5 +``` + +## Component Descriptions + +1. **Contact Form Component**: The React component that collects user input and sends it to the API route. +2. **API Route**: A Next.js API route that receives form submissions and uses the Contact Service Factory to process them. +3. **Contact Service Factory**: Creates the appropriate implementation of the IContactService interface based on configuration. +4. **IContactService Interface**: Defines the contract for sending contact form data. +5. **Discord Contact Service**: Implements the IContactService interface to send notifications to Discord via webhooks. +6. **Email Contact Service**: (Future) Implements the IContactService interface to send notifications via email. +7. **Discord Webhook**: The Discord webhook endpoint that receives the notifications. +8. **Email Service**: (Future) An email service that would send email notifications. + +## Data Flow + +1. User fills out the contact form and submits it +2. The form component sends a POST request to the `/api/contact` endpoint +3. The API route uses the Contact Service Factory to get the appropriate service implementation +4. The service sends the notification to the configured channel (e.g., Discord) +5. The API route returns a success/error response to the form component +6. The form component displays a success/error message to the user diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 000000000..47f9acc47 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,72 @@ +# Soul Solidity Website Architecture + +This document provides an overview of the Soul Solidity website architecture, focusing on key components and their interactions. + +## Technology Stack + +- **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 + +## Key Components + +### Contact Service + +The contact service provides a way to send contact form submissions to various notification channels. It uses a dependency injection pattern to allow for multiple implementations. + +```mermaid +graph TD + A[Contact Form Component] --> B[API Route] + B --> C[Contact Service Factory] + C --> D[IContactService Interface] + D --> E[Discord Contact Service] + D -.-> F[Email Contact Service] + F -.-> G[Future Implementations] + + style F stroke-dasharray: 5 5 + style G stroke-dasharray: 5 5 +``` + +#### Components + +1. **Contact Form Component**: The React component that collects user input and sends it to the API route. +2. **API Route**: A Next.js API route that receives form submissions and uses the Contact Service Factory to process them. +3. **Contact Service Factory**: Creates the appropriate implementation of the IContactService interface based on configuration. +4. **IContactService Interface**: Defines the contract for sending contact form data. +5. **Discord Contact Service**: Implements the IContactService interface to send notifications to Discord via webhooks. +6. **Email Contact Service**: (Future) Implements the IContactService interface to send notifications via email. + +## Directory Structure + +``` +app/ +├── api/ +│ └── contact/ +│ └── route.ts # API route for contact form submissions +├── components/ +│ └── contact.tsx # Contact form component +└── services/ + └── contact/ + ├── IContactService.ts # Contact service interface + ├── ContactServiceFactory.ts # Factory for creating contact services + ├── DiscordContactService.ts # Discord implementation + └── EmailContactService.ts # (Future) Email implementation +``` + +## Data Flow + +1. User fills out the contact form and submits it +2. The form component sends a POST request to the `/api/contact` endpoint +3. The API route uses the Contact Service Factory to get the appropriate service implementation +4. The service sends the notification to the configured channel (e.g., Discord) +5. The API route returns a success/error response to the form component +6. The form component displays a success/error message to the user + +## Security Considerations + +- Sensitive configuration (like webhook URLs) is stored in environment variables on the server +- Form validation is performed both client-side and server-side +- Rate limiting is implemented to prevent abuse +- No sensitive information is exposed to the client diff --git a/docs/integration/discord-webhook.md b/docs/integration/discord-webhook.md new file mode 100644 index 000000000..2100febe1 --- /dev/null +++ b/docs/integration/discord-webhook.md @@ -0,0 +1,114 @@ +# Discord Webhook Integration Guide + +This guide provides step-by-step instructions for setting up a Discord webhook to receive contact form submissions from the Soul Solidity website. + +## Prerequisites + +- Discord account +- Administrative access to a Discord server (or the ability to create one) + +## Setting Up a Discord Server (if needed) + +If you don't already have a Discord server: + +1. Open Discord and log in to your account +2. Click the "+" icon on the left sidebar +3. Select "Create My Own" +4. Enter a name for your server and click "Create" + +## Creating a Webhook in Discord + +1. Open Discord and navigate to the server where you want to receive notifications +2. Select the text channel where you want the contact form submissions to appear +3. Right-click on the channel and select "Edit Channel" +4. Click on "Integrations" in the left sidebar +5. Click on "Webhooks" +6. Click the "New Webhook" button +7. Configure the webhook: + - Set a name (e.g., "Soul Solidity Contact Form") + - Optionally, upload an avatar image + - Make note of which channel it will post to +8. Click "Save Changes" +9. Click "Copy Webhook URL" to copy the webhook URL to your clipboard + +## Required Permissions + +To create a webhook, you need the "Manage Webhooks" permission in the Discord server. This permission is typically available to server administrators and moderators. + +## Setting Up Role Mentions (Optional) + +If you want the webhook to mention a specific role when a new contact form submission is received: + +1. Go to your Discord server settings +2. Click on "Roles" in the left sidebar +3. Create a new role or select an existing one (e.g., "Contact Notifications") +4. Assign this role to the team members who should be notified +5. Make sure the role is mentionable by webhooks: + - In the role settings, enable "Allow anyone to @mention this role" +6. Copy the role ID: + - Enable Developer Mode in Discord (User Settings > Advanced > Developer Mode) + - Right-click on the role in the server settings and select "Copy ID" + +## Configuring the Website + +After obtaining the webhook URL and (optionally) the role ID, you need to add them to the website's environment variables: + +1. Create or edit the `.env.local` file in the root of the project +2. Add the following variables: + + ``` + DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your-webhook-url + DISCORD_NOTIFICATION_ROLE_ID=123456789012345678 # Optional: Role ID to mention + ``` + +3. Restart the development server or redeploy the application + +## Security Best Practices + +### Protecting the Webhook URL + +The webhook URL should be treated as a secret: + +- Never commit the webhook URL to version control +- Store it in environment variables or a secure secrets manager +- Limit access to the webhook URL to authorized personnel + +### Webhook URL Rotation + +Consider rotating the webhook URL periodically: + +1. Create a new webhook in Discord +2. Update the environment variable with the new URL +3. Delete the old webhook + +### Rate Limiting + +The contact form implements rate limiting to prevent abuse. If you experience issues with legitimate submissions being blocked, you may need to adjust the rate limiting settings. + +## Testing the Webhook + +To test if the webhook is working correctly: + +1. Fill out and submit the contact form on the website +2. Check the Discord channel to see if the message appears +3. Verify that the message contains all the expected information + +## Troubleshooting + +### Message Not Appearing in Discord + +- Verify that the webhook URL is correct +- Check if the channel still exists +- Ensure the webhook has permission to post in the channel +- Check server-side logs for any errors + +### Role Mentions Not Working + +- Verify that the role ID is correct +- Ensure the role is mentionable by webhooks +- Check if the role still exists in the server + +## Additional Resources + +- [Discord Webhook Documentation](https://discord.com/developers/docs/resources/webhook) +- [Discord Developer Portal](https://discord.com/developers/applications) diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts new file mode 100644 index 000000000..45ed8d095 --- /dev/null +++ b/tests/navigation.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Navigation tests", () => { + test("Gold chrome button is visible on desktop", async ({ page }) => { + // Set desktop viewport + await page.setViewportSize({ width: 1280, height: 720 }); + + // Navigate to homepage + await page.goto("/"); + + // Wait for page to load + await page.waitForLoadState("networkidle"); + + // Check that the gold chrome button is visible + await expect(page.locator('text="Soul Solidity"').first()).toBeVisible(); + }); + + test("Gold chrome button is visible on mobile", async ({ page }) => { + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + // Navigate to homepage + await page.goto("/"); + + // Wait for page to load + await page.waitForLoadState("networkidle"); + + // Check that the gold chrome button is visible + await expect(page.locator('text="Soul Solidity"').first()).toBeVisible(); + }); +});