Skip to content
Draft
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
134 changes: 134 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# NutriPlan - Supabase & Stripe Integration

This project is a sample implementation of a web application named NutriPlan. It demonstrates how to integrate Supabase for user authentication and database operations (managing orders) and Stripe for handling payments (subscriptions via Stripe Checkout).

The frontend is built with plain HTML, CSS, and JavaScript, focusing on a clean, modern, and mobile-first design.

## Features

* **User Authentication:** Sign-up, login, and logout functionality managed by Supabase Auth.
* **Product Display:** A clean, responsive grid displaying available subscription plans.
* **Stripe Checkout:** Secure payment processing through redirection to Stripe Checkout.
* **Order History:** Logged-in users can view their past orders, fetched from a Supabase database.
* **Secure API Calls:** The architecture is designed to use a serverless backend (like Supabase Edge Functions) to securely create Stripe Checkout sessions, preventing keys from being exposed on the client-side.

## Tech Stack

* **Frontend:** HTML, CSS, JavaScript
* **Authentication & Database:** [Supabase](https://supabase.io/)
* **Payments:** [Stripe](https://stripe.com/)
* **Fonts:** Google Fonts (Montserrat & Open Sans)

## Project Structure

```
.
├── index.html # The main HTML file for the user interface.
├── style.css # The stylesheet for the application.
├── script.js # The core JavaScript file for all client-side logic.
└── README.md # This documentation file.
```

---

## Installation and Setup

Follow these instructions to get the project running locally.

### Prerequisites

* A [Supabase](https://app.supabase.io/) account.
* A [Stripe](https://dashboard.stripe.com/register) account.
* A local web server to serve the files (e.g., VS Code Live Server, Python's `http.server`).

### Step 1: Set up Supabase

1. **Create a New Project:**
* Go to your Supabase dashboard and create a new project.

2. **Get API Keys:**
* Navigate to your project's **Settings > API**.
* You will need the **Project URL** and the **`anon` public key**.

3. **Create an `orders` Table:**
* Go to the **Table Editor** in your Supabase project.
* Create a new table named `orders`.
* Add the following columns:
* `id` (int8, is identity) - Primary Key
* `created_at` (timestamptz, default `now()`)
* `user_id` (uuid, foreign key to `auth.users.id`) - This links the order to a user.
* `product_name` (text) - The name of the purchased plan.
* `stripe_payment_intent` (text) - The payment intent ID from Stripe for reference.
* Make sure to **disable Row Level Security (RLS)** for this table for initial testing, or create appropriate policies for access. For production, you should enable RLS and create policies that only allow users to see their own orders.

### Step 2: Set up Stripe

1. **Get API Keys:**
* Go to your Stripe Dashboard.
* Navigate to **Developers > API keys**.
* You will need the **Publishable key** (`pk_test_...`).

2. **Create Products:**
* In the Stripe Dashboard, go to the **Products** catalog.
* Create at least two products (e.g., "Basic Plan", "Premium Plan").
* For each product, add a pricing plan (e.g., $9.99/month).
* Note down the **API ID** for each price (e.g., `price_123abc`). You will need this for the checkout button in `index.html`.

### Step 3: Configure the Project

1. **Clone or Download the Repository:**
* Get the project files onto your local machine.

2. **Update `index.html`:**
* Open `index.html`.
* Find the `.product-card` divs.
* Update the `data-product-id` attribute with the **Price API IDs** you got from Stripe.
```html
<div class="product-card" data-product-id="YOUR_STRIPE_PRICE_ID_HERE">
```

3. **Update `script.js`:**
* Open `script.js`.
* At the top of the file, replace the placeholder constants with your actual keys from Supabase and Stripe.
```javascript
const SUPABASE_URL = 'YOUR_SUPABASE_URL'; // From Supabase Settings > API
const SUPABASE_ANON_KEY = 'YOUR_SUPABASE_ANON_KEY'; // From Supabase Settings > API
const STRIPE_PUBLISHABLE_KEY = 'YOUR_STRIPE_PUBLISHABLE_KEY'; // From Stripe Developers > API Keys
```

### Step 4: (Advanced) Create the Backend Function

For the Stripe Checkout to work, you need a secure backend environment to create the checkout session. The client-side code in `script.js` is prepared to call a serverless function.

1. **Create a Supabase Edge Function:**
* Use the Supabase CLI to create a new function (e.g., `create-checkout-session`).
* This function will receive a `productId` from the client.
* Inside the function, use the Stripe Node.js library to create a checkout session. You will need your **Stripe Secret Key** here (store it as a Supabase secret).
* The function should return the `sessionId` to the client.

2. **Client-side Call:**
* In `script.js`, uncomment and use the `supabase.functions.invoke` call to trigger your edge function.

---

## Testing

1. **Start a Local Server:**
* From the project's root directory, start a simple web server. For example, if you have Python 3:
```bash
python -m http.server
```
* Open your browser and navigate to `http://localhost:8000` (or the appropriate port).

2. **Test Authentication:**
* Since the example uses a dummy login, you'll need to either implement a full Supabase Auth UI or manually create a user in your Supabase dashboard and log in via the browser console to get a session.
* Once logged in, your email should appear, and the "Your Orders" section should be visible.

3. **Test Checkout:**
* Click on a "Choose Plan" button.
* Check the browser's console. You should see a log message indicating a redirect would happen.
* If you have implemented the Supabase Edge Function, you should be redirected to the Stripe Checkout page.

4. **Test Order History:**
* After a successful (test) payment, you'll need a mechanism (like a Stripe webhook) to write the order details back to your Supabase `orders` table.
* Once an order is in the table for the logged-in user, it should appear in the "Your Orders" section.
148 changes: 45 additions & 103 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,121 +1,63 @@
<!DOCTYPE html>
<html lang="ru">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NutriPlan - Программа 'Фитнес'</title>
<title>NutriPlan - Your Personal Meal Plan</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Lato:wght@400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Open+Sans:wght@400&display=swap" rel="stylesheet">
</head>
<body>

<main class="product-page">
<div class="product-gallery">
<img src="https://images.unsplash.com/photo-1546069901-ba9599a7e63c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1780&q=80" alt="Пример блюда из программы 'Фитнес'">
<header>
<h1>NutriPlan</h1>
<div id="auth-container">
<!-- Auth UI will be dynamically inserted here by Supabase -->
</div>

<div class="product-calculator">
<header class="calculator-header">
<h1>Программа питания 'Фитнес'</h1>
<p>Идеальный баланс для поддержания формы и достижения спортивных результатов. Начните свой путь к здоровью уже сегодня!</p>
</header>

<form id="nutriplan-form">
<!-- Step 1: Calories -->
<fieldset class="form-step">
<legend>1. Калорийность</legend>
<div class="options-group">
<label>
<input type="radio" name="calories" value="1500" data-modifier="0" checked>
<span class="option-card">1500 ккал</span>
</label>
<label>
<input type="radio" name="calories" value="1800" data-modifier="200">
<span class="option-card">1800 ккал</span>
</label>
<label>
<input type="radio" name="calories" value="2200" data-modifier="400">
<span class="option-card">2200 ккал</span>
</label>
</div>
</fieldset>

<!-- Step 2: Meals per Day -->
<fieldset class="form-step">
<legend>2. Количество приемов пищи</legend>
<div class="options-group">
<label>
<input type="radio" name="meals" value="3" data-modifier="0" checked>
<span class="option-card">3</span>
</label>
<label>
<input type="radio" name="meals" value="4" data-modifier="200">
<span class="option-card">4</span>
</label>
<label>
<input type="radio" name="meals" value="5" data-modifier="400">
<span class="option-card">5</span>
</label>
</div>
</fieldset>

<!-- Step 3: Duration -->
<fieldset class="form-step">
<legend>3. Продолжительность</legend>
<div class="options-group">
<label>
<input type="radio" name="duration" value="5" checked>
<span class="option-card">5 дней</span>
</label>
<label>
<input type="radio" name="duration" value="7">
<span class="option-card">7 дней</span>
</label>
<label>
<input type="radio" name="duration" value="14">
<span class="option-card">14 дней</span>
</label>
<label>
<input type="radio" name="duration" value="30">
<span class="option-card">30 дней</span>
</label>
</div>
</fieldset>

<!-- Step 4: Extras -->
<fieldset class="form-step">
<legend>4. Дополнительно</legend>
<div class="options-group">
<label>
<input type="checkbox" name="extras" value="cutlery" data-modifier="50">
<span class="option-card">Столовые приборы</span>
</label>
<label>
<input type="checkbox" name="extras" value="detox" data-modifier="150">
<span class="option-card">Детокс-напиток</span>
</label>
</div>
</fieldset>
</form>

<div class="price-display">
<div class="price-item">
<span>Цена за день:</span>
<span id="price-per-day">1000 ₽</span>
</header>

<main>
<section id="user-info" class="hidden">
<h2>Welcome, <span id="user-email"></span>!</h2>
<button id="logout-button">Logout</button>
</section>

<section id="products">
<h2>Our Plans</h2>
<div class="product-grid">
<!-- Products will be dynamically inserted here -->
<div class="product-card" data-product-id="price_123abc">
<h3>Basic Plan</h3>
<p class="price">$9.99/month</p>
<p>Get a personalized weekly meal plan.</p>
<button class="checkout-button">Choose Plan</button>
</div>
<div class="price-total">
<span>Итоговая стоимость:</span>
<span id="total-price">5000 ₽</span>
<div class="product-card" data-product-id="price_456def">
<h3>Premium Plan</h3>
<p class="price">$19.99/month</p>
<p>Weekly meal plan + personal nutritionist chat.</p>
<button class="checkout-button">Choose Plan</button>
</div>
</div>

<button type="submit" form="nutriplan-form" class="cta-button">Оформить заказ</button>

</div>
</section>

<section id="orders" class="hidden">
<h2>Your Orders</h2>
<ul id="order-list">
<!-- Orders will be dynamically inserted here -->
</ul>
</section>
</main>

<footer>
<p>&copy; 2024 NutriPlan. All rights reserved.</p>
</footer>

<!-- Supabase and Stripe JS libraries -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="https://js.stripe.com/v3/"></script>
<script src="script.js"></script>
</body>
</html>
Binary file added jules-scratch/verification/verification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions jules-scratch/verification/verify_nutriplan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from playwright.sync_api import sync_playwright, Page, expect

def run(playwright):
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()

# Navigate to the local server
page.goto("http://localhost:8000")

# Wait for the main heading to be visible to ensure the page is loaded
expect(page.get_by_role("heading", name="NutriPlan")).to_be_visible()

# Take a screenshot
page.screenshot(path="jules-scratch/verification/verification.png")

browser.close()

with sync_playwright() as playwright:
run(playwright)
Loading