diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 74de096..dd37d82 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,7 +4,7 @@ name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: - branches: ['master'] + branches: ["master"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -17,7 +17,7 @@ permissions: # Allow one concurrent deployment concurrency: - group: 'pages' + group: "pages" cancel-in-progress: true jobs: @@ -25,12 +25,13 @@ jobs: deploy: environment: name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest env: VITE_APP_SPOTIFY_CLIENT_ID: ${{ secrets.VITE_APP_SPOTIFY_CLIENT_ID }} VITE_APP_SPOTIFY_CLIENT_SECRET: ${{ secrets.VITE_APP_SPOTIFY_CLIENT_SECRET }} VITE_APP_SPOTIFY_REFRESH_TOKEN: ${{ secrets.VITE_APP_SPOTIFY_REFRESH_TOKEN }} + VITE_APP_CLOUD_NAME: ${{ secrets.VITE_APP_CLOUD_NAME }} steps: - name: Checkout uses: actions/checkout@v4 @@ -38,7 +39,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 21 - cache: 'npm' + cache: "npm" - name: Install dependencies run: npm ci - name: Build @@ -52,7 +53,7 @@ jobs: uses: actions/upload-pages-artifact@v3 with: # Upload dist folder - path: './dist' + path: "./dist" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/package-lock.json b/package-lock.json index 8960a9d..28018c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mui/lab": "^5.0.0-alpha.170", "axios": "^1.6.8", "buffer": "^6.0.3", + "cloudinary-build-url": "^0.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.8.2", @@ -429,6 +430,14 @@ "statuses": "^2.0.1" } }, + "node_modules/@cld-apis/utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@cld-apis/utils/-/utils-0.2.0.tgz", + "integrity": "sha512-WBhOZ+wz93G/vsqkfmIBsPYf7FH4i5ZKN3QYlUfg9Ni5A2E09CCfsNG6RUuGeLjDPaFXFbta1rkNf3+2hFlavQ==", + "dependencies": { + "snake-case": "^3.0.4" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -2947,6 +2956,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cloudinary-build-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/cloudinary-build-url/-/cloudinary-build-url-0.2.4.tgz", + "integrity": "sha512-Wi0aZPHOa90xDCCC4k9eoTVPIm3aWNyycO+rJUXvOyusQqmkPSprHSuJh3s2zNxEtuGj24aCM13iLDpilWMpSw==", + "dependencies": { + "@cld-apis/utils": "^0.2.0" + } + }, "node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -3176,6 +3193,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.736", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.736.tgz", @@ -4340,6 +4366,14 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4639,6 +4673,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -5364,6 +5407,15 @@ "node": ">=8" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5598,6 +5650,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 97312e1..20d74cf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@mui/lab": "^5.0.0-alpha.170", "axios": "^1.6.8", "buffer": "^6.0.3", + "cloudinary-build-url": "^0.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.8.2", diff --git a/src/assets/mt-dark.png b/src/assets/mt-dark.png deleted file mode 100644 index 7cc07f3..0000000 Binary files a/src/assets/mt-dark.png and /dev/null differ diff --git a/src/assets/mt-light.png b/src/assets/mt-light.png deleted file mode 100644 index 140c4f9..0000000 Binary files a/src/assets/mt-light.png and /dev/null differ diff --git a/src/components/CloudinaryImage.tsx b/src/components/CloudinaryImage.tsx new file mode 100644 index 0000000..d52407f --- /dev/null +++ b/src/components/CloudinaryImage.tsx @@ -0,0 +1,18 @@ +import { getCloudinaryUrl } from "../services/cloudinary"; + +type CloudinaryImageProps = { + publicId: string; + alt: string; + options?: Record; + className?: string; +}; + +export default function CloudinaryImage({ + publicId, + alt, + options, + className, +}: CloudinaryImageProps) { + const imageUrl = getCloudinaryUrl(publicId, options); + return {alt}; +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 5c56a2b..5763a71 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import { Typography, Container, Stack, IconButton } from "@mui/material"; -import MTLogoDark from "../../assets/mt-dark.png"; -import MTLogoLight from "../../assets/mt-light.png"; import { ThemeContext } from "../../mui/theme-context"; +import { CloudinaryImage } from "../../components"; export default function Footer() { const { theme } = React.useContext(ThemeContext); @@ -11,8 +10,8 @@ export default function Footer() { - Maria Torrente diff --git a/src/components/index.ts b/src/components/index.ts index 37e75ad..9e1ea07 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,3 +4,4 @@ export { default as Experience } from "./Experience/Experience"; export { default as Projects } from "./Projects/Projects"; export { default as SpotifyNowPlaying } from "./SpotifyNowPlaying/SpotifyNowPlaying"; export { default as Footer } from "./Footer/Footer"; +export { default as CloudinaryImage } from "./CloudinaryImage"; diff --git a/src/config.ts b/src/config.ts index 9ce4154..cc0e818 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,10 @@ -export const CLIENT_ID = import.meta.env.VITE_APP_SPOTIFY_CLIENT_ID; -export const CLIENT_SECRET = import.meta.env.VITE_APP_SPOTIFY_CLIENT_SECRET; -export const REFRESH_TOKEN = import.meta.env.VITE_APP_SPOTIFY_REFRESH_TOKEN; -export const ENV = import.meta.env.VITE_APP_NODE_ENV; -export const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token"; -export const NOW_PLAYING_ENDPOINT = - "https://api.spotify.com/v1/me/player/currently-playing"; +export const config = { + CLIENT_ID: import.meta.env.VITE_APP_SPOTIFY_CLIENT_ID, + CLIENT_SECRET: import.meta.env.VITE_APP_SPOTIFY_CLIENT_SECRET, + REFRESH_TOKEN: import.meta.env.VITE_APP_SPOTIFY_REFRESH_TOKEN, + ENV: import.meta.env.VITE_APP_NODE_ENV, + CLOUD_NAME: import.meta.env.VITE_APP_CLOUD_NAME, + TOKEN_ENDPOINT: "https://accounts.spotify.com/api/token", + NOW_PLAYING_ENDPOINT: + "https://api.spotify.com/v1/me/player/currently-playing", +}; diff --git a/src/services/cloudinary.ts b/src/services/cloudinary.ts new file mode 100644 index 0000000..17a8e10 --- /dev/null +++ b/src/services/cloudinary.ts @@ -0,0 +1,24 @@ +import { buildUrl } from "cloudinary-build-url"; +import { config } from "../config"; + +type TransformationOptions = { + [key: string]: unknown; +}; + +export function getCloudinaryUrl( + publicId: string, + options?: TransformationOptions +): string { + if (!config.CLOUD_NAME) { + throw new Error( + "Cloudinary cloud name is required to compose the image url." + ); + } + + return buildUrl(publicId, { + cloud: { + cloudName: config.CLOUD_NAME, + }, + transformations: options, + }); +} diff --git a/src/services/spotify.ts b/src/services/spotify.ts index e42313f..4f60119 100644 --- a/src/services/spotify.ts +++ b/src/services/spotify.ts @@ -1,13 +1,7 @@ import type { NowPlayingItem } from "../types/spotify"; import axios from "axios"; import { Buffer } from "buffer"; -import { - TOKEN_ENDPOINT, - NOW_PLAYING_ENDPOINT, - CLIENT_ID as clientId, - CLIENT_SECRET as clientSecret, - REFRESH_TOKEN as refreshToken, -} from "../config"; +import { config } from "../config"; export type GetAccessTokenResponse = { access_token: string; @@ -18,17 +12,19 @@ export type GetAccessTokenResponse = { }; export const getAccessToken = async () => { - const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); + const basic = Buffer.from( + `${config.CLIENT_ID}:${config.CLIENT_SECRET}` + ).toString("base64"); const bodyParams = new URLSearchParams({ grant_type: "refresh_token", - refresh_token: refreshToken, + refresh_token: config.REFRESH_TOKEN, }); const body = bodyParams.toString(); try { const response = await axios.post( - TOKEN_ENDPOINT, + config.TOKEN_ENDPOINT, body, { headers: { @@ -57,7 +53,7 @@ export const getNowPlaying = async (accessToken?: string) => { throw "No Access Token available."; } const response = await axios.get( - NOW_PLAYING_ENDPOINT, + config.NOW_PLAYING_ENDPOINT, { headers: { Authorization: `Bearer ${accessToken}`,