From 71a4a425e8fd3e976dd6cbb2bdebbb8f4e34a628 Mon Sep 17 00:00:00 2001 From: Sergey Todyshev Date: Sun, 26 Sep 2021 09:52:21 +0300 Subject: [PATCH] wip uploadcare storage provider --- .gitignore | 1 + package.json | 2 + src/axios-utils.ts | 11 +++++ src/components/CourseView.tsx | 4 +- src/components/Loader.tsx | 33 +++++++++++++++ src/components/UploadcareFileGroups.tsx | 34 +++++++++++++++ src/components/YouTubePlaylists.tsx | 47 +++++++++++++++++++++ src/pages/course/[pid].tsx | 44 ------------------- src/pages/course/uploadcare/[id].tsx | 51 ++++++++++++++++++++++ src/pages/course/youtube/[pid].tsx | 47 +++++++++++++++++++++ src/pages/index.tsx | 56 +++++++------------------ src/types.ts | 3 +- src/uploadcare-api.ts | 30 +++++++++++++ src/youtube-api.ts | 3 ++ yarn.lock | 28 ++++++++++++- 15 files changed, 307 insertions(+), 87 deletions(-) create mode 100644 src/axios-utils.ts create mode 100644 src/components/Loader.tsx create mode 100644 src/components/UploadcareFileGroups.tsx create mode 100644 src/components/YouTubePlaylists.tsx delete mode 100644 src/pages/course/[pid].tsx create mode 100644 src/pages/course/uploadcare/[id].tsx create mode 100644 src/pages/course/youtube/[pid].tsx create mode 100644 src/uploadcare-api.ts diff --git a/.gitignore b/.gitignore index 20fccdd..6697b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ # misc .DS_Store +.idea # debug npm-debug.log* diff --git a/package.json b/package.json index 8e6170b..599ac30 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "lodash": "4.17.21", "next": "11.1.2", "react": "17.0.2", + "react-content-loader": "6.0.3", "react-dom": "17.0.2", "react-google-login": "5.2.2", + "react-player": "2.9.0", "react-youtube": "7.13.1", "recoil": "0.4.1", "swr": "1.0.1" diff --git a/src/axios-utils.ts b/src/axios-utils.ts new file mode 100644 index 0000000..18bb015 --- /dev/null +++ b/src/axios-utils.ts @@ -0,0 +1,11 @@ +import { AxiosResponse } from "axios"; + +function isStatusOK(status: number) { + return status >= 200 && status < 300; +} + +export function checkStatusOK(resp: AxiosResponse) { + if (!isStatusOK(resp.status)) { + throw new Error(JSON.stringify(resp.data)); + } +} diff --git a/src/components/CourseView.tsx b/src/components/CourseView.tsx index f59fd2b..c4a5978 100644 --- a/src/components/CourseView.tsx +++ b/src/components/CourseView.tsx @@ -12,6 +12,7 @@ import { TabPanels, TabPanel, } from "@chakra-ui/react"; +import Player from "react-player"; import YouTube from "react-youtube"; import { Course, Lesson } from "types"; @@ -33,8 +34,9 @@ const CourseView: React.FC = ({ data }) => { {lesson?.youtubeVideoId ? ( - + ) : null} + {lesson?.videoUrl ? : null} {/* lesson list */} diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx new file mode 100644 index 0000000..b25eedb --- /dev/null +++ b/src/components/Loader.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import ContentLoader from "react-content-loader"; + +const Default = (props) => ( + + + + + + + + + + +); + +type Props = { + [prop: string]: any; +}; + +const Loader: React.FC = ({ ...props }) => { + return ; +}; + +export default Loader; diff --git a/src/components/UploadcareFileGroups.tsx b/src/components/UploadcareFileGroups.tsx new file mode 100644 index 0000000..5ba5a3d --- /dev/null +++ b/src/components/UploadcareFileGroups.tsx @@ -0,0 +1,34 @@ +import _ from "lodash"; +import Link from "next/link"; +import useSWR from "swr"; +import { Box } from "@chakra-ui/react"; +import { getFileGroups } from "uploadcare-api"; +import ErrorView from "./ErrorView"; +import Loader from "./Loader"; + +const UploadcareFileGroups = ({ apiConfig }) => { + const { data, error } = useSWR("/uc/file-groups", () => + getFileGroups(apiConfig) + ); + return ( + <> + + {!error && !data && } + {!error && data && _.isEmpty(data) ? ( + You don't have file groups yet. + ) : null} + {_.map(data?.results, (item, k) => { + const id = item.id.replace("~1", ""); + return ( + + + {item.title || id} + + + ); + })} + + ); +}; + +export default UploadcareFileGroups; diff --git a/src/components/YouTubePlaylists.tsx b/src/components/YouTubePlaylists.tsx new file mode 100644 index 0000000..dd605a0 --- /dev/null +++ b/src/components/YouTubePlaylists.tsx @@ -0,0 +1,47 @@ +import _ from "lodash"; +import Link from "next/link"; +import { Box } from "@chakra-ui/react"; +import useSWR from "swr"; +import { getPlaylists } from "youtube-api"; +import { useAccessToken } from "components/GoogleAuth"; +import ErrorView from "./ErrorView"; +import Loader from "./Loader"; + +const YouTubePlaylists = () => { + const accessToken = useAccessToken(); + const { data, error } = useSWR( + `/playlists?token=${accessToken}`, + async () => { + if (!accessToken) { + return []; + } + const resp = await getPlaylists({ + accessToken, + }); + return resp.items.map((t) => ({ + id: t.id, + title: t.snippet.title, + description: t.snippet.description, + })); + } + ); + return ( + <> + + {!error && !data && } + {!error && data && _.isEmpty(data) ? ( + + No data. Please login into your google account to view your video + courses + + ) : null} + {_.map(data, (item, k) => ( + + {item.title} + + ))} + + ); +}; + +export default YouTubePlaylists; diff --git a/src/pages/course/[pid].tsx b/src/pages/course/[pid].tsx deleted file mode 100644 index f295aa0..0000000 --- a/src/pages/course/[pid].tsx +++ /dev/null @@ -1,44 +0,0 @@ -import _ from "lodash"; -import { useRouter } from "next/router"; -import CourseView from "components/CourseView"; -import { useAccessToken } from "components/GoogleAuth"; -import ErrorView from "components/ErrorView"; -import Page from "components/Layout"; -import useSWR from "swr"; -import { getPlaylistItems, getPlaylists } from "youtube-api"; -import { Course } from "types"; - -export default function CoursePage() { - const router = useRouter(); - const { pid } = router.query; - const accessToken = useAccessToken(); - const { data, error } = useSWR(`/course?token=${accessToken}`, async () => { - if (!accessToken) { - throw new Error("no access token"); - } - // TODO get playlist by id - const list = await getPlaylists({ accessToken }); - const playlist = _.find(list.items, (t) => t.id === pid); - const resp = await getPlaylistItems({ - playlistId: String(pid), - accessToken, - }); - return { - title: playlist?.snippet?.title || "", - description: playlist?.snippet?.description || "", - lessons: resp.items.map((t) => ({ - title: t.snippet.title, - description: t.snippet.description, - duration: 5, - youtubeVideoId: t.contentDetails.videoId, - })), - } as Course; - }); - - return ( - - - {data ? : null} - - ); -} diff --git a/src/pages/course/uploadcare/[id].tsx b/src/pages/course/uploadcare/[id].tsx new file mode 100644 index 0000000..e9a3628 --- /dev/null +++ b/src/pages/course/uploadcare/[id].tsx @@ -0,0 +1,51 @@ +import _ from "lodash"; +import { useRouter } from "next/router"; +import CourseView from "components/CourseView"; +import ErrorView from "components/ErrorView"; +import Page from "components/Layout"; +import useSWR from "swr"; +import { getFileGroup } from "uploadcare-api"; +import { Course } from "types"; + +export async function getStaticPaths() { + return { + paths: ["/course/uploadcare/1"], + fallback: true, + }; +} + +export async function getStaticProps() { + return { + props: { + uploadcare: { + publicKey: process.env.UC_PUBLIC_KEY, + secretKey: process.env.UC_SECRET_KEY, + }, + }, + }; +} + +export default function CoursePage({ uploadcare }) { + const router = useRouter(); + const { id } = router.query; + const { data, error } = useSWR(`/uploadcare/file-group/${id}`, async () => { + const grp = await getFileGroup(String(id), uploadcare); + return { + title: grp.title || grp.id, + description: grp.description || "", + lessons: _.map(grp.items, (t) => ({ + title: t.snippet.title, + description: t.snippet.description, + duration: 5, + youtubeVideoId: t.contentDetails.videoId, + })), + } as Course; + }); + + return ( + + + {data ? : null} + + ); +} diff --git a/src/pages/course/youtube/[pid].tsx b/src/pages/course/youtube/[pid].tsx new file mode 100644 index 0000000..53cb877 --- /dev/null +++ b/src/pages/course/youtube/[pid].tsx @@ -0,0 +1,47 @@ +import _ from "lodash"; +import { useRouter } from "next/router"; +import CourseView from "components/CourseView"; +import { useAccessToken } from "components/GoogleAuth"; +import ErrorView from "components/ErrorView"; +import Page from "components/Layout"; +import useSWR from "swr"; +import { getPlaylistItems, getPlaylists } from "youtube-api"; +import { Course } from "types"; + +export default function CoursePage() { + const router = useRouter(); + const { pid } = router.query; + const accessToken = useAccessToken(); + const { data, error } = useSWR( + `/youtube/course?token=${accessToken}`, + async () => { + if (!accessToken) { + throw new Error("no access token"); + } + // TODO get playlist by id + const list = await getPlaylists({ accessToken }); + const playlist = _.find(list.items, (t) => t.id === pid); + const resp = await getPlaylistItems({ + playlistId: String(pid), + accessToken, + }); + return { + title: playlist?.snippet?.title || "", + description: playlist?.snippet?.description || "", + lessons: resp.items.map((t) => ({ + title: t.snippet.title, + description: t.snippet.description, + duration: 5, + youtubeVideoId: t.contentDetails.videoId, + })), + } as Course; + } + ); + + return ( + + + {data ? : null} + + ); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 45ae206..c94fd6f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,4 @@ import _ from "lodash"; -import Link from "next/link"; import { Box, Stack, @@ -9,59 +8,36 @@ import { TabPanels, TabPanel, } from "@chakra-ui/react"; -import ErrorView from "components/ErrorView"; -import { useAccessToken } from "components/GoogleAuth"; import Page from "components/Layout"; -import useSWR from "swr"; -import { getPlaylists } from "youtube-api"; +import YouTubePlaylists from "components/YouTubePlaylists"; +import UCFileGroups from "components/UploadcareFileGroups"; -export default function Home() { - const accessToken = useAccessToken(); - const { data, error } = useSWR( - `/playlists?token=${accessToken}`, - async () => { - if (!accessToken) { - return []; - } - const resp = await getPlaylists({ - accessToken, - }); - return resp.items.map((t) => ({ - id: t.id, - title: t.snippet.title, - description: t.snippet.description, - })); - } - ); +export async function getStaticProps() { + return { + props: { + uploadcare: { + publicKey: process.env.UC_PUBLIC_KEY, + secretKey: process.env.UC_SECRET_KEY, + }, + }, + }; +} +export default function Home({ uploadcare }) { return ( - - + YouTube UploadCare - {_.isEmpty(data) ? ( - - No data. Please login into your google account to view your - video courses - - ) : null} - {_.map(data, (item) => ( - - {item.title} - - ))} + - - No data. Please login into your uploadcare account to view your - video courses - + diff --git a/src/types.ts b/src/types.ts index f9a9720..b38f579 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,8 @@ export type Lesson = { title: string; description: string; duration: number; - youtubeVideoId: string; + youtubeVideoId?: string; + videoUrl?: string; }; export type Course = { diff --git a/src/uploadcare-api.ts b/src/uploadcare-api.ts new file mode 100644 index 0000000..a3ec1b2 --- /dev/null +++ b/src/uploadcare-api.ts @@ -0,0 +1,30 @@ +import axios from "axios"; +import { checkStatusOK } from "axios-utils"; + +type ApiConfig = { + publicKey: string; + secretKey: string; +}; + +function makeClient(config: ApiConfig) { + return axios.create({ + headers: { + Accept: "application/vnd.uploadcare-v0.5+json", + Authorization: `Uploadcare.Simple ${config.publicKey}:${config.secretKey}`, + }, + }); +} + +export async function getFileGroups(config: ApiConfig) { + const http = makeClient(config); + const resp = await http.get("https://api.uploadcare.com/groups/"); + checkStatusOK(resp); + return resp.data; +} + +export async function getFileGroup(id: string, config: ApiConfig) { + const http = makeClient(config); + const resp = await http.get(`https://api.uploadcare.com/groups/${id}`); + checkStatusOK(resp); + return resp.data; +} diff --git a/src/youtube-api.ts b/src/youtube-api.ts index 302f8f5..9a06082 100644 --- a/src/youtube-api.ts +++ b/src/youtube-api.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { checkStatusOK } from "axios-utils"; function makeClient(accessToken: string) { return axios.create({ @@ -29,6 +30,7 @@ export async function getPlaylists({ }, } ); + checkStatusOK(resp); return resp.data; } @@ -54,5 +56,6 @@ export async function getPlaylistItems({ }, } ); + checkStatusOK(resp); return resp.data; } diff --git a/yarn.lock b/yarn.lock index 092954b..65b6e8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1382,6 +1382,11 @@ debug@2, debug@^2.6.6: dependencies: ms "2.0.0" +deepmerge@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2055,6 +2060,11 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -2493,6 +2503,11 @@ react-clientside-effect@^1.2.2: dependencies: "@babel/runtime" "^7.12.13" +react-content-loader@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.0.3.tgz#32e28ca7120e0a2552fc26655d0d4448cc1fc0c5" + integrity sha512-CIRgTHze+ls+jGDIfCitw27YkW2XcaMpsYORTUdBxsMFiKuUYMnlvY76dZE4Lsaa9vFXVw+41ieBEK7SJt0nug== + react-dom@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -2502,7 +2517,7 @@ react-dom@17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-fast-compare@3.2.0: +react-fast-compare@3.2.0, react-fast-compare@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== @@ -2537,6 +2552,17 @@ react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-player@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.9.0.tgz#ef7fe7073434087565f00ff219824e1e02c4b046" + integrity sha512-jNUkTfMmUhwPPAktAdIqiBcVUKsFKrVGH6Ocutj6535CNfM91yrvWxHg6fvIX8Y/fjYUPoejddwh7qboNV9vGA== + dependencies: + deepmerge "^4.0.0" + load-script "^1.0.0" + memoize-one "^5.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.0.1" + react-refresh@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"