-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
27dd793
commit 8bb1b77
Showing
26 changed files
with
1,370 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { | ||
updateProfile, | ||
updateProfileByCustomerId | ||
} from "../db/queries/profiles-queries" | ||
import { Membership } from "../types/membership" | ||
import Stripe from "stripe" | ||
import { stripe } from "../lib/stripe" | ||
|
||
const getMembershipStatus = ( | ||
status: Stripe.Subscription.Status, | ||
membership: Membership | ||
): Membership => { | ||
switch (status) { | ||
case "active": | ||
case "trialing": | ||
return membership | ||
case "canceled": | ||
case "incomplete": | ||
case "incomplete_expired": | ||
case "past_due": | ||
case "paused": | ||
case "unpaid": | ||
return "free" | ||
default: | ||
return "free" | ||
} | ||
} | ||
|
||
export const updateStripeCustomer = async ( | ||
profileId: string, | ||
subscriptionId: string, | ||
customerId: string | ||
) => { | ||
const subscription = await stripe.subscriptions.retrieve(subscriptionId, { | ||
expand: ["default_payment_method"] | ||
}) | ||
|
||
const updatedProfile = await updateProfile(profileId, { | ||
stripeCustomerId: customerId, | ||
stripeSubscriptionId: subscription.id | ||
}) | ||
|
||
if (!updatedProfile) { | ||
throw new Error("Failed to update customer") | ||
} | ||
} | ||
|
||
export const manageSubscriptionStatusChange = async ( | ||
subscriptionId: string, | ||
customerId: string, | ||
productId: string | ||
) => { | ||
const subscription = await stripe.subscriptions.retrieve(subscriptionId, { | ||
expand: ["default_payment_method"] | ||
}) | ||
|
||
const product = await stripe.products.retrieve(productId) | ||
const membership = product.metadata.membership as Membership | ||
|
||
const membershipStatus = getMembershipStatus(subscription.status, membership) | ||
|
||
await updateProfileByCustomerId(customerId, { | ||
stripeSubscriptionId: subscription.id, | ||
membership: membershipStatus | ||
}) | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { stripe } from "../../../../lib/stripe" | ||
import { | ||
manageSubscriptionStatusChange, | ||
updateStripeCustomer | ||
} from "../../../../actions/stripe-actions" | ||
import { headers } from "next/headers" | ||
import Stripe from "stripe" | ||
|
||
const relevantEvents = new Set([ | ||
"checkout.session.completed", | ||
"customer.subscription.updated", | ||
"customer.subscription.deleted" | ||
]) | ||
|
||
export async function POST(req: Request) { | ||
const body = await req.text() | ||
const sig = headers().get("Stripe-Signature") as string | ||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET | ||
let event: Stripe.Event | ||
|
||
try { | ||
if (!sig || !webhookSecret) { | ||
throw new Error("Webhook secret or signature missing") | ||
} | ||
|
||
event = stripe.webhooks.constructEvent(body, sig, webhookSecret) | ||
} catch (err: any) { | ||
return new Response(`Webhook Error: ${err.message}`, { status: 400 }) | ||
} | ||
|
||
if (relevantEvents.has(event.type)) { | ||
try { | ||
switch (event.type) { | ||
case "customer.subscription.updated": | ||
case "customer.subscription.deleted": | ||
const subscription = event.data.object as Stripe.Subscription | ||
const productId = subscription.items.data[0].price.product as string | ||
|
||
await manageSubscriptionStatusChange( | ||
subscription.id, | ||
subscription.customer as string, | ||
productId | ||
) | ||
|
||
break | ||
|
||
case "checkout.session.completed": | ||
const checkoutSession = event.data.object as Stripe.Checkout.Session | ||
if (checkoutSession.mode === "subscription") { | ||
const subscriptionId = checkoutSession.subscription | ||
|
||
await updateStripeCustomer( | ||
checkoutSession.client_reference_id as string, | ||
subscriptionId as string, | ||
checkoutSession.customer as string | ||
) | ||
|
||
const subscription = await stripe.subscriptions.retrieve( | ||
subscriptionId as string, | ||
{ | ||
expand: ["default_payment_method"] | ||
} | ||
) | ||
|
||
const productId = subscription.items.data[0].price.product as string | ||
|
||
await manageSubscriptionStatusChange( | ||
subscription.id, | ||
subscription.customer as string, | ||
productId | ||
) | ||
} | ||
|
||
break | ||
|
||
default: | ||
throw new Error("Unhandled relevant event!") | ||
} | ||
} catch (error) { | ||
return new Response( | ||
"Webhook handler failed. View your nextjs function logs.", | ||
{ | ||
status: 400 | ||
} | ||
) | ||
} | ||
} | ||
|
||
return new Response(JSON.stringify({ received: true })) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
'use client' | ||
|
||
import { useEffect, useState } from 'react' | ||
import { createClient } from '@supabase/supabase-js' | ||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' | ||
|
||
// Initialize Supabase client | ||
const supabase = createClient( | ||
process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! | ||
) | ||
|
||
interface BiofeedbackEntry { | ||
date: string | ||
hunger_score: number | ||
digestion_score: number | ||
sleep_quality_score: number | ||
energy_levels_score: number | ||
gym_performance_score: number | null | ||
} | ||
|
||
export default function BiofeedbackChart() { | ||
const [data, setData] = useState<BiofeedbackEntry[]>([]) | ||
|
||
useEffect(() => { | ||
const fetchData = async () => { | ||
const { data, error } = await supabase | ||
.from('biofeedback') | ||
.select('date, hunger_score, digestion_score, sleep_quality_score, energy_levels_score, gym_performance_score') | ||
.order('date', { ascending: true }) | ||
|
||
if (error) { | ||
console.error('Error fetching data:', error) | ||
} else { | ||
setData(data) | ||
} | ||
} | ||
|
||
fetchData() | ||
|
||
// Set up real-time listener | ||
const subscription = supabase | ||
.channel('biofeedback_changes') | ||
.on('postgres_changes', { event: '*', schema: 'public', table: 'biofeedback' }, fetchData) | ||
.subscribe() | ||
|
||
return () => { | ||
subscription.unsubscribe() | ||
} | ||
}, []) | ||
|
||
return ( | ||
<ResponsiveContainer width="100%" height={400}> | ||
<LineChart data={data}> | ||
<CartesianGrid strokeDasharray="3 3" /> | ||
<XAxis dataKey="date" /> | ||
<YAxis domain={[0, 10]} /> | ||
<Tooltip /> | ||
<Legend /> | ||
<Line type="monotone" dataKey="hunger_score" stroke="#8884d8" /> | ||
<Line type="monotone" dataKey="digestion_score" stroke="#82ca9d" /> | ||
<Line type="monotone" dataKey="sleep_quality_score" stroke="#ffc658" /> | ||
<Line type="monotone" dataKey="energy_levels_score" stroke="#ff7300" /> | ||
<Line type="monotone" dataKey="gym_performance_score" stroke="#00C49F" /> | ||
</LineChart> | ||
</ResponsiveContainer> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import BiofeedbackChart from './BiofeedbackChart' | ||
|
||
export default function BiofeedbackPage() { | ||
return ( | ||
<div className="container mx-auto p-4"> | ||
<h1 className="text-2xl font-bold mb-4">Biofeedback Tracking</h1> | ||
<BiofeedbackChart /> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { ButtonHTMLAttributes, ReactNode } from 'react'; | ||
|
||
export const Button = ({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & { children: ReactNode }) => { | ||
return ( | ||
<button {...props}> | ||
{children} | ||
</button> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import React from 'react'; // {{ edit_1 }} | ||
|
||
export function Input({ ...props }) { | ||
return <input {...props} className="input" /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
'use client' | ||
|
||
import { useState } from 'react' | ||
import { Button } from '@/app/components/ui/button' | ||
import { createClient } from '@supabase/supabase-js' | ||
import { Input } from '@/app/components/ui/input' | ||
import { useAuth } from '@clerk/nextjs' | ||
|
||
// Initialize Supabase client | ||
const supabase = createClient( | ||
process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! | ||
) | ||
|
||
interface BiofeedbackData { | ||
date: string | ||
time: string | ||
metrics: { | ||
hunger_levels: { score: number; notes: string } | ||
digestion: { score: number; notes: string } | ||
sleep_quality: { score: number; notes: string } | ||
energy_levels: { score: number; notes: string } | ||
gym_performance: { score: number | null; notes: string } | ||
} | ||
additional_notes: string[] | ||
summary: string | ||
} | ||
|
||
export default function JsonUploadForm() { | ||
const { getToken } = useAuth() | ||
const [jsonData, setJsonData] = useState('') | ||
const [isLoading, setIsLoading] = useState(false) | ||
const [error, setError] = useState<string | null>(null) | ||
|
||
const handleSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault() | ||
setIsLoading(true) | ||
setError(null) | ||
|
||
try { | ||
const token = await getToken({ template: 'supabase' }) | ||
if (!token) throw new Error('Not authenticated') | ||
|
||
supabase.auth.setAuth(token) | ||
|
||
const data: BiofeedbackData = JSON.parse(jsonData) | ||
|
||
const { error } = await supabase.from('biofeedback').insert({ | ||
date: new Date(`${data.date}T${data.time}`), | ||
time: new Date(`${data.date}T${data.time}`), | ||
hunger_score: data.metrics.hunger_levels.score, | ||
hunger_notes: data.metrics.hunger_levels.notes, | ||
digestion_score: data.metrics.digestion.score, | ||
digestion_notes: data.metrics.digestion.notes, | ||
sleep_quality_score: data.metrics.sleep_quality.score, | ||
sleep_quality_notes: data.metrics.sleep_quality.notes, | ||
energy_levels_score: data.metrics.energy_levels.score, | ||
energy_levels_notes: data.metrics.energy_levels.notes, | ||
gym_performance_score: data.metrics.gym_performance.score, | ||
gym_performance_notes: data.metrics.gym_performance.notes, | ||
additional_notes: data.additional_notes, | ||
summary: data.summary, | ||
}) | ||
|
||
if (error) throw error | ||
|
||
setJsonData('') | ||
alert('Data uploaded successfully!') | ||
} catch (err) { | ||
setError(err instanceof Error ? err.message : 'An error occurred') | ||
} finally { | ||
setIsLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<form onSubmit={handleSubmit} className="space-y-4"> | ||
<textarea | ||
value={jsonData} | ||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setJsonData(e.target.value)} | ||
placeholder="Paste your JSON data here" | ||
rows={10} | ||
className="w-full p-2 border rounded" | ||
required | ||
/> | ||
<Button type="submit" disabled={isLoading}> | ||
{isLoading ? 'Uploading...' : 'Upload Data'} | ||
</Button> | ||
{error && <p className="text-red-500">{error}</p>} | ||
</form> | ||
) | ||
} |
Oops, something went wrong.