diff --git a/webdev-prosjekt1/.gitignore b/webdev-prosjekt1/.gitignore index a547bf3..19862e3 100644 --- a/webdev-prosjekt1/.gitignore +++ b/webdev-prosjekt1/.gitignore @@ -16,6 +16,7 @@ dist-ssr .vscode/* !.vscode/extensions.json .idea +.env .DS_Store *.suo *.ntvs* diff --git a/webdev-prosjekt1/package-lock.json b/webdev-prosjekt1/package-lock.json index f7f3f88..c8fbc13 100644 --- a/webdev-prosjekt1/package-lock.json +++ b/webdev-prosjekt1/package-lock.json @@ -27,7 +27,7 @@ "globals": "^17.11.0", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.3.0" } }, "node_modules/@babel/code-frame": { @@ -2203,9 +2203,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -2329,9 +2329,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -2349,7 +2349,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2611,16 +2611,16 @@ } }, "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -2637,7 +2637,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/webdev-prosjekt1/package.json b/webdev-prosjekt1/package.json index c5d5b5c..00003ce 100644 --- a/webdev-prosjekt1/package.json +++ b/webdev-prosjekt1/package.json @@ -29,6 +29,6 @@ "globals": "^17.11.0", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.3.0" } } diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 27caa3e..2493cb3 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -3,6 +3,7 @@ import { Button } from './components/Button'; import { Searchbar } from './components/Searchbar'; import filterIcon from './assets/filter.png'; import musicWizImage from './assets/musicwiz.png'; +import { searchTrack, searchAlbum, searchArtist } from "./api/spotify" function App() { return ( diff --git a/webdev-prosjekt1/src/api/spotify.ts b/webdev-prosjekt1/src/api/spotify.ts new file mode 100644 index 0000000..a85d102 --- /dev/null +++ b/webdev-prosjekt1/src/api/spotify.ts @@ -0,0 +1,107 @@ +import type { Album, Artist, Track } from "./types"; + + +const BASE_URL = "https://api.spotify.com/v1" +const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID; +const CLIENT_SECRET = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET; +const LIMIT = 10; + +let accessToken: string | null = null; +let expiresAt = 0; + +async function getSpotifyToken() { + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + }), + }); + if (!response.ok) { + throw new Error(`Failed to get Spotify token: ${response.status}`); + } + + return response.json(); +} + +async function getValidToken() { + if (accessToken && Date.now() < expiresAt) { + return accessToken; + } + + const tokenData = await getSpotifyToken(); + + accessToken = tokenData.access_token; + expiresAt = Date.now() + tokenData.expires_in * 1000; + + return accessToken; +} + + + +export async function searchArtist(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=artist&limit=${LIMIT}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Spotify api error: ${response.status}`); + } + + const json = await response.json(); + const artists: Artist[] = json.artists.items; + console.log(artists); + + return artists; +} + + +export async function searchTrack(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=track&limit=${LIMIT}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Spotify api error: ${response.status}`); + } + + const json = await response.json(); + const tracks: Track[] = json.tracks.items; + console.log(tracks); + + return tracks; +} + + +export async function searchAlbum(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=album&limit=${LIMIT}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Spotify api error: ${response.status}`); + } + + const json = await response.json(); + const albums: Album[] = json.albums.items; + console.log(albums); + + return albums; +} + diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts new file mode 100644 index 0000000..e2cad8a --- /dev/null +++ b/webdev-prosjekt1/src/api/types.ts @@ -0,0 +1,58 @@ +// This file defines api related data model. + +export type Artist = { + id: number; + name: string; + type: string; + href: string; + uri: string; + images: Image[]; +} + + +export type Track = { + id: string; + name: string; + type: string; + href: string; + artists: { + id: number; + name: string; + type: string; + href: string; + uri: string; + }[]; + album: Album; + duration_ms: number; + explicit: boolean; + track_number: number; + external_ids: { + isrc: string; + }; + is_playable: boolean; + uri: string; +} + +export type Album = { + id: string; + name: string; + album_type: "album" | "single" | "compilation"; + artists: { + id: number; + name: string; + type: string; + href: string; + uri: string; + }[]; + images: Image[]; + release_date: string; + release_date_precision: "year" | "month" | "day"; + total_tracks: number; + uri: string; +}; + +export type Image = { + url: string; + height: number; + width: number; +} \ No newline at end of file