Skip to content

Commit

Permalink
Merge pull request #18 from StackOverflowIsBetterThanAnyAI/16-add-bac…
Browse files Browse the repository at this point in the history
…kend-for-deployment

migrate api logic to external backend on vercel
  • Loading branch information
StackOverflowIsBetterThanAnyAI authored Jul 29, 2024
2 parents 663c920 + bbd43bb commit e7ca493
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 120 deletions.
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
# dependencies
/node_modules

# production
/build

# api
/src/clientdata
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"postcss": "^8.4.39",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -16,10 +17,13 @@
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"proxy": "http://localhost:5000",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"author": "Michael Münzenhofer",
"license": "MIT",
"eslintConfig": {
"extends": [
"react-app"
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const App = () => {
}, [language, seoSearchText])

return (
<div className="min-w-72 min-h-screen bg-zinc-800">
<div className="min-w-64 min-h-screen bg-zinc-800">
<ContextScreenWidth.Provider value={screenWidth}>
<ContextLanguage.Provider value={[language, setLanguage]}>
<ContextErrorMessage.Provider
Expand Down
2 changes: 1 addition & 1 deletion src/components/UI/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const Navigation = () => {
const searchMobileRef = useRef<HTMLInputElement>(null)
const buttonIconRef = useRef<HTMLButtonElement>(null)
const userIconRef = useRef<HTMLButtonElement>(null)
const anchorRef = useRef<HTMLAnchorElement>(null)
const anchorRef = useRef<HTMLButtonElement>(null)
const inputRef = useRef<HTMLInputElement>(null)

const handleBlur = () => {
Expand Down
69 changes: 39 additions & 30 deletions src/components/UI/navigation/UserIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ADDRESS, CLIENT_ID } from './../../../clientdata/clientdata'
import {
Dispatch,
FC,
Expand All @@ -19,7 +18,7 @@ export const ContextFilterLanguageExpanded = createContext<
>(undefined)

type UserIconProps = {
anchorRef: React.RefObject<HTMLAnchorElement>
anchorRef: React.RefObject<HTMLButtonElement>
buttonRef: React.RefObject<HTMLButtonElement>
}

Expand All @@ -31,6 +30,7 @@ export const UserIcon: FC<UserIconProps> = ({ anchorRef, buttonRef }) => {
return storedUser ? JSON.parse(storedUser) : null
})
const [dropdownActive, setDropdownActive] = useState(false)
const [redirectUrl, setRedirectUrl] = useState<string | null>(null)

const popupRef = useRef<HTMLDivElement | null>(null)

Expand All @@ -42,7 +42,7 @@ export const UserIcon: FC<UserIconProps> = ({ anchorRef, buttonRef }) => {

const fetchUser = async () => {
try {
const data = await getUser(CLIENT_ID || '')
const data = await getUser()
if (!data)
throw new Error('Unable to fetch the currently logged in user')
setUser(data)
Expand Down Expand Up @@ -88,7 +88,7 @@ export const UserIcon: FC<UserIconProps> = ({ anchorRef, buttonRef }) => {
}
}

const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === ' ') {
const target = e.target as HTMLElement
target.click()
Expand All @@ -97,6 +97,29 @@ export const UserIcon: FC<UserIconProps> = ({ anchorRef, buttonRef }) => {
}
}

useEffect(() => {
const getAuthUrl = async () => {
if (state.length) {
try {
const response = await fetch(
`https://twitch-backend.vercel.app/api/auth-url?state=${state}`
)
const data = await response.json()
setRedirectUrl(data.url)
} catch (error) {
console.error('Error fetching auth URL:', error)
}
}
}
getAuthUrl()
}, [state])

useEffect(() => {
if (redirectUrl) {
window.location.href = redirectUrl
}
}, [redirectUrl])

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
Expand Down Expand Up @@ -182,38 +205,24 @@ export const UserIcon: FC<UserIconProps> = ({ anchorRef, buttonRef }) => {
</>
) : (
<>
<a
href={`https://id.twitch.tv/oauth2/authorize?response_type=token&client_id=${
CLIENT_ID || ''
}&redirect_uri=${
ADDRESS || 'http://localhost:3000'
}&state=${state}&scope=user:read:email`}
className="rounded-md px-2 pseudo-zinc navigation"
onKeyDown={handleKeyDown}
onClick={handleAnchorClick}
title="Log in"
ref={anchorRef}
<button
className="rounded-md px-2 pseudo-zinc"
onClick={handleButtonClick}
ref={buttonRef}
>
<img
src={getImage('', 48, 'PROFILE')}
src={getImage(
user?.profile_image_url || '',
48,
'PROFILE'
)}
alt="Settings"
title="Settings"
loading="lazy"
width={48}
className="rounded-full"
/>
</a>
{sessionStorage.getItem('twitch_logged_in') ===
'false' && (
<div
className={`absolute top-16 right-4 bg-zinc-700 p-2 border-2 border-zinc-50 animate-fadeOut`}
>
<h2 className="text-base lg:text-lg">
Login failed.
</h2>
<h3 className="text-sm lg:text-base">
Please try again later.
</h3>
</div>
)}
</button>
</>
)}
</ContextFilterLanguageExpanded.Provider>
Expand Down
4 changes: 1 addition & 3 deletions src/components/streamFeed/StreamFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import StreamTags from './StreamTags'
import StreamThumbnail from './StreamThumbnail'
import StreamTitle from './StreamTitle'
import StreamViewerCount from './StreamViewerCount'

import { CLIENT_ID, CLIENT_SECRET } from '../../clientdata/clientdata'
import {
ContextErrorMessage,
ContextFilteredStreamData,
Expand Down Expand Up @@ -125,7 +123,7 @@ const StreamFeed = () => {
const url = `https://api.twitch.tv/helix/streams?language=${language}`
try {
const data: StreamProps | { error: 'login' } | undefined =
await getStreams(CLIENT_ID || '', CLIENT_SECRET || '', url)
await getStreams(url)
if (data && 'error' in data && data.error === 'login') {
setStreamData(undefined)
setFilteredStreamData(undefined)
Expand Down
8 changes: 1 addition & 7 deletions src/components/streamFeed/StreamProfilePicture.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { FC, useContext, useEffect, useState } from 'react'
import { getProfilePicture } from '../../helper/getProfilePicture'

import { CLIENT_ID, CLIENT_SECRET } from '../../clientdata/clientdata'

import { getImage } from '../../helper/getImage'
import { ContextScreenWidth } from '../../App'

Expand All @@ -26,11 +24,7 @@ const StreamProfilePicture: FC<StreamProfilePictureProps> = ({
useEffect(() => {
const fetchImageUrl = async () => {
try {
const data = await getProfilePicture(
CLIENT_ID || '',
CLIENT_SECRET || '',
user_id || ''
)
const data = await getProfilePicture(user_id || '')
if (!data) {
throw new Error()
} else setImageUrl(data)
Expand Down
41 changes: 18 additions & 23 deletions src/helper/getProfilePicture.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
import axios from 'axios'
import { AuthorizationProps } from '../types/AuthorizationProps'
import { ProfilePictureProps } from '../types/ProfilePictureProps'
import { getTwitchAuthorization } from './getTwitchAuthorization'

export const getProfilePicture = async (
CLIENT_ID: string,
CLIENT_SECRET: string,
user_id: string
): Promise<string | undefined> => {
const url = `https://api.twitch.tv/helix/users?id=${user_id}`

const authorizationObject: AuthorizationProps =
await getTwitchAuthorization(CLIENT_ID, CLIENT_SECRET)
await getTwitchAuthorization()
let { access_token, token_type } = authorizationObject
if (access_token === '' && token_type === '') return undefined
if (!access_token || !token_type) return undefined

token_type =
token_type.substring(0, 1).toUpperCase() +
token_type.substring(1, token_type.length)

const authorization = `${token_type} ${access_token}`
try {
const response = await axios.post(
'https://twitch-backend.vercel.app/api/profile-picture',
{
access_token,
token_type,
user_id,
}
)

const headers = {
authorization,
'Client-ID': CLIENT_ID,
}
if (response.status !== 200 || typeof response.data !== 'object') {
throw new Error(`${response.status}`)
}

try {
const response = await fetch(url, {
headers,
method: 'GET',
})
if (!response.ok) throw new Error(`${response.status} ${response.url}`)
const data: ProfilePictureProps = await response.json()
const imageUrl: string = data.data[0].profile_image_url
return imageUrl
} catch (error: any) {
return response.data.imageUrl
} catch (error) {
console.error(
'The following error occured while fetching a user profile picture for the user:',
'The following error occurred while fetching a user profile picture for the user:',
user_id,
error
)
Expand Down
41 changes: 21 additions & 20 deletions src/helper/getStreams.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
import axios from 'axios'
import { AuthorizationProps } from '../types/AuthorizationProps'
import { getTwitchAuthorization } from './getTwitchAuthorization'

import type { StreamProps } from '../types/StreamProps'

export const getStreams = async (
CLIENT_ID: string,
CLIENT_SECRET: string,
url: string
): Promise<StreamProps | undefined | { error: 'login' }> => {
const authorizationObject: AuthorizationProps =
await getTwitchAuthorization(CLIENT_ID, CLIENT_SECRET)
await getTwitchAuthorization()
let { access_token, token_type } = authorizationObject
if (access_token === '' && token_type === '') return { error: 'login' }
if (!access_token || !token_type) return { error: 'login' }

token_type =
token_type.substring(0, 1).toUpperCase() +
token_type.substring(1, token_type.length)

const authorization = `${token_type} ${access_token}`
try {
const response = await axios.post(
'https://twitch-backend.vercel.app/api/streams',
{
access_token,
token_type,
url,
}
)

const headers = {
authorization,
'Client-ID': CLIENT_ID,
}
if (response.status !== 200 || typeof response.data !== 'object') {
throw new Error(`${response.status}`)
}

try {
const response = await fetch(url, {
headers,
method: 'GET',
})
if (!response.ok) throw new Error(`${response.status} ${response.url}`)
const data: StreamProps = await response.json()
if (!data.data.length)
if (!response.data.data)
throw new Error(
`There are no Livestreams available under this url: ${response.url}`
`There are no Livestreams available under this url: ${url}`
)
return data

return response.data
} catch (error: any) {
console.error(
'The following error occured while fetching the current Livestreams:',
error
)
}

return undefined
}
Loading

0 comments on commit e7ca493

Please sign in to comment.