diff --git a/package-lock.json b/package-lock.json index c57b655..231d1e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "clsx": "^2.1.1", "next": "16.1.1", + "next-auth": "^4.24.13", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" @@ -231,6 +232,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1229,6 +1239,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2632,6 +2651,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4421,6 +4449,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5023,6 +5060,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5058,6 +5127,12 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5068,6 +5143,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5181,6 +5265,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5347,6 +5473,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.28.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.1.tgz", + "integrity": "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5357,6 +5505,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6408,6 +6562,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c770abe..0bde52d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "clsx": "^2.1.1", "next": "16.1.1", + "next-auth": "^4.24.13", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..6158ef4 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,29 @@ +import NextAuth from "next-auth" +import TwitchProvider from "next-auth/providers/twitch" + +const handler = NextAuth({ + providers: [ + TwitchProvider({ + clientId: process.env.TWITCH_CLIENT_ID!, + clientSecret: process.env.TWITCH_CLIENT_SECRET!, + }), + ], + callbacks: { + async session({ session, token }) { + if (session?.user) { + // Pass the user ID to the session + // Note: We might need to extend the type definition for session.user + (session.user as any).id = token.sub; + } + return session; + }, + async jwt({ token, user }) { + if (user) { + token.id = user.id; + } + return token; + }, + }, +}) + +export { handler as GET, handler as POST } diff --git a/src/app/api/twitch/status/route.ts b/src/app/api/twitch/status/route.ts new file mode 100644 index 0000000..461cc7e --- /dev/null +++ b/src/app/api/twitch/status/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getTwitchStatus } from '@/lib/twitch'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const username = searchParams.get('username'); + + if (!username) { + return NextResponse.json({ error: 'Username is required' }, { status: 400 }); + } + + const isLive = await getTwitchStatus(username); + + return NextResponse.json({ isLive }); +} diff --git a/src/components/AuthButtons.tsx b/src/components/AuthButtons.tsx new file mode 100644 index 0000000..fdd7488 --- /dev/null +++ b/src/components/AuthButtons.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { signIn, signOut, useSession } from "next-auth/react"; +import Link from "next/link"; + +export function LoginButton() { + return ( + + ); +} + +export function LogoutButton() { + return ( + + ); +} + +export function UserProfile() { + const { data: session } = useSession(); + + if (!session?.user) return null; + + return ( +
+ {session.user.image && ( + {session.user.name + )} +
+ + {session.user.name} + + +
+
+ ); +} diff --git a/src/components/dashboard/StyleControls.tsx b/src/components/dashboard/StyleControls.tsx index 39a64f6..53c8a66 100644 --- a/src/components/dashboard/StyleControls.tsx +++ b/src/components/dashboard/StyleControls.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { OverlaySettings } from '@/lib/types'; +import SupportModule from './SupportModule'; interface StyleControlsProps { settings: OverlaySettings; @@ -209,6 +210,8 @@ export default function StyleControls({ settings, updateSettings }: StyleControl 7TV Emotes + + ); } diff --git a/src/components/dashboard/SupportModule.tsx b/src/components/dashboard/SupportModule.tsx new file mode 100644 index 0000000..207adea --- /dev/null +++ b/src/components/dashboard/SupportModule.tsx @@ -0,0 +1,104 @@ +"use client"; + +import React, { useEffect, useState } from 'react'; + +const DEVELOPER_USERNAME = 'ramforth'; // Hardcoded developer username + +export default function SupportModule() { + const [isLive, setIsLive] = useState(false); + + useEffect(() => { + const checkStatus = async () => { + try { + const res = await fetch(`/api/twitch/status?username=${DEVELOPER_USERNAME}`); + const data = await res.json(); + setIsLive(data.isLive); + } catch (error) { + console.error('Failed to check Twitch status:', error); + } + }; + + checkStatus(); + // Poll every 5 minutes + const interval = setInterval(checkStatus, 5 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + return ( +
+

+ Support & Social +

+ +
+ {/* Live Indicator / Watch Button */} + +
+ {/* Avatar / Icon Placeholder */} +
+ {/* Simple Twitch Icon SVG */} + + + +
+ +
+ + ramforth + + Developer +
+
+ + {isLive ? ( +
+ + + + + LIVE +
+ ) : ( + + Offline + + )} +
+ + {/* Action Buttons */} +
+ + Follow + + + + ❤️ Donate + +
+
+
+ ); +} diff --git a/src/lib/twitch.ts b/src/lib/twitch.ts new file mode 100644 index 0000000..cf2d9af --- /dev/null +++ b/src/lib/twitch.ts @@ -0,0 +1,54 @@ +// src/lib/twitch.ts + +export async function getTwitchStatus(username: string) { + const clientId = process.env.TWITCH_CLIENT_ID; + const clientSecret = process.env.TWITCH_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + console.error('Missing Twitch API credentials'); + return false; + } + + try { + // 1. Get App Access Token + const tokenResponse = await fetch('https://id.twitch.tv/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + grant_type: 'client_credentials', + }), + }); + + if (!tokenResponse.ok) { + throw new Error('Failed to get Twitch access token'); + } + + const tokenData = await tokenResponse.json(); + const accessToken = tokenData.access_token; + + // 2. Check Stream Status + const streamResponse = await fetch(`https://api.twitch.tv/helix/streams?user_login=${username}`, { + headers: { + 'Client-ID': clientId, + 'Authorization': `Bearer ${accessToken}`, + }, + }); + + if (!streamResponse.ok) { + throw new Error('Failed to fetch stream status'); + } + + const streamData = await streamResponse.json(); + + // If the data array is not empty, the user is live + return streamData.data && streamData.data.length > 0; + + } catch (error) { + console.error('Error fetching Twitch status:', error); + return false; + } +}