├── config.ts
├── src
├── react-app-env.d.ts
├── service
│ └── index.ts
├── setupTests.js
├── components
│ ├── Navbar.tsx
│ ├── Header.tsx
│ ├── Toggle.tsx
│ └── Player.tsx
├── App.tsx
├── reportWebVitals.js
├── index.js
├── hooks
│ ├── useToggle.ts
│ ├── useMedia.ts
│ ├── useLocalStorage.ts
│ ├── useDarkmode.ts
│ └── useFetch.ts
├── pages
│ └── Playlist.tsx
└── index.css
├── public
├── robots.txt
├── Eater.mp3
├── Neon.mp3
├── Space.mp3
├── cover.jpg
├── favicon.ico
├── logo192.png
├── logo512.png
├── Playlist.jpg
├── manifest.json
└── index.html
├── .gitignore
├── tsconfig.json
├── package.json
├── README.md
└── server
└── db.json
/config.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/Eater.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/Eater.mp3
--------------------------------------------------------------------------------
/public/Neon.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/Neon.mp3
--------------------------------------------------------------------------------
/public/Space.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/Space.mp3
--------------------------------------------------------------------------------
/public/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/cover.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/Playlist.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/React-Hooks-com-Typescript-2021-06-30/HEAD/public/Playlist.jpg
--------------------------------------------------------------------------------
/src/service/index.ts:
--------------------------------------------------------------------------------
1 | export const axiosOptions = {
2 | baseURL: "http://localhost:3001/",
3 | timeout: 15000,
4 | };
5 |
6 | export const tracksUrl = "tracks";
7 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Toggle from "./Toggle";
3 | import useDarkMode from "../hooks/useDarkmode";
4 |
5 | export default function Navbar() {
6 | const [darkMode, setDarkMode] = useDarkMode();
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Playlist from "./pages/Playlist";
4 | import Header from "./components/Header";
5 | import Navbar from "./components/Navbar";
6 |
7 | function App() {
8 | return (
9 |
16 | );
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function Header() {
4 | return (
5 | <>
6 |
7 |
8 |
This is
9 |
Experts Club
10 |
9999 Monthly Listeners
11 |
12 |
13 |
14 |
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/hooks/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | // Hook
3 | // Parameter is the boolean, with default "false" value
4 | const useToggle = (initialState: boolean = false): [boolean, () => void] => {
5 | // Initialize the state
6 | const [state, setState] = useState(initialState);
7 |
8 | // Define and memorize toggler function in case we pass down the comopnent,
9 | // This function change the boolean value to it's opposite value
10 | const toggle = useCallback((): void => setState((state) => !state), []);
11 | return [state, toggle];
12 | };
13 |
14 | export default useToggle;
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Toggle = (props: any) => (
4 |
5 |
8 |
9 | props.setDarkMode(!props.darkMode)}
15 | />
16 |
17 |
18 |
21 |
22 | );
23 |
24 | export default Toggle;
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "share-hooks-with-typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@react-firebase/firestore": "^0.5.5",
7 | "@testing-library/jest-dom": "^5.11.4",
8 | "@testing-library/react": "^11.1.0",
9 | "@testing-library/user-event": "^12.1.10",
10 | "@types/react": "^17.0.11",
11 | "axios": "^0.21.1",
12 | "firebase": "^8.6.8",
13 | "json-server": "^0.16.3",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2",
16 | "react-scripts": "4.0.3",
17 | "react-use-audio-player": "^1.2.4",
18 | "typescript": "^4.3.4",
19 | "web-vitals": "^1.0.1"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject",
26 | "start:server": "json-server --watch server/db.json --port 3001"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/hooks/useMedia.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useMedia = (queries: string[], values: T[], defaultValue: T) => {
4 |
5 | // Array containing a media query list for each query
6 | const mediaQueryLists = queries.map((q) => window.matchMedia(q));
7 |
8 | // Function that gets value based on matching media query
9 | const getValue = () => {
10 | // Get index of first media query that matches
11 | const index = mediaQueryLists.findIndex((mql) => mql.matches);
12 | // Return related value or defaultValue if none
13 | return values?.[index] || defaultValue;
14 | };
15 |
16 | // State and setter for matched value
17 | const [value, setValue] = useState(getValue);
18 |
19 | useEffect(() => {
20 | // Event listener callback
21 | // Note: By defining getValue outside of useEffect we ensure that it has ...
22 | // ... current values of hook args (as this hook callback is created once on mount).
23 | const handler = () => setValue(getValue);
24 | // Set a listener for each media query with above handler as callback.
25 | mediaQueryLists.forEach((mql) => mql.addListener(handler));
26 | // Remove listeners on cleanup
27 | return () => mediaQueryLists.forEach((mql) => mql.removeListener(handler));
28 | });
29 | return value;
30 | };
31 |
32 | export default useMedia;
33 |
--------------------------------------------------------------------------------
/src/components/Player.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useAudioPlayer } from "react-use-audio-player";
3 |
4 | const AudioPlayer = ({
5 | file,
6 | title,
7 | artWork,
8 | }: {
9 | file?: string;
10 | title?: string;
11 | artWork?: string;
12 | }) => {
13 | const { togglePlayPause, ready, loading, playing } = useAudioPlayer({
14 | src: file,
15 | autoplay: true,
16 | onend: () => console.log("sound has ended!"),
17 | });
18 |
19 | if (!ready && !loading) return No audio to play
;
20 | if (loading) return Loading audio
;
21 |
22 | return (
23 |
24 |
25 |
26 |

33 |
34 |
{title}
35 |
36 |
37 |
38 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default AudioPlayer;
50 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | function useLocalStorage(key: string, initialValue: T) {
4 | // State to store our value
5 | // Pass initial state function to useState so logic is only executed once
6 | const [storedValue, setStoredValue] = useState(() => {
7 | try {
8 | // Get from local storage by key
9 | const item = window.localStorage.getItem(key);
10 | // Parse stored json or if none return initialValue
11 | return item ? JSON.parse(item) : initialValue;
12 | } catch (error) {
13 | // If error also return initialValue
14 | console.log(error);
15 | return initialValue;
16 | }
17 | });
18 |
19 | // Return a wrapped version of useState's setter function that ...
20 | // ... persists the new value to localStorage.
21 | const setValue = (value: T | ((val: T) => T)) => {
22 | try {
23 | // Allow value to be a function so we have same API as useState
24 | const valueToStore = value instanceof Function ? value(storedValue) : value;
25 |
26 | // Save state
27 | setStoredValue(valueToStore);
28 |
29 | // Save to local storage
30 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
31 | } catch (error) {
32 | // A more advanced implementation would handle the error case
33 | console.log(error);
34 | }
35 | };
36 | return [storedValue, setValue] as const;
37 | }
38 |
39 | export default useLocalStorage;
40 |
--------------------------------------------------------------------------------
/src/hooks/useDarkmode.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import useLocalStorage from "./useLocalStorage";
3 | import useMedia from "./useMedia";
4 |
5 | // Hook
6 | function useDarkMode() {
7 | // Use our useLocalStorage hook to persist state through a page refresh.
8 | const [enabledState, setEnabledState] = useLocalStorage(
9 | "dark-mode-enabled",
10 | false
11 | );
12 |
13 | // See if user has set a browser or OS preference for dark mode.
14 | // The usePrefersDarkMode hook composes a useMedia hook (see code below).
15 | const prefersDarkMode = usePrefersDarkMode();
16 |
17 | // If enabledState is defined use it, otherwise fallback to prefersDarkMode.
18 | // This allows user to override OS level setting on our website.
19 | const enabled = enabledState ?? prefersDarkMode;
20 |
21 | // Fire off effect that add/removes dark mode class
22 | useEffect(
23 | () => {
24 | const className = "dark-mode";
25 | const element = window.document.body;
26 | if (enabled) {
27 | element.classList.add(className);
28 | } else {
29 | element.classList.remove(className);
30 | }
31 | },
32 | [enabled] // Only re-call effect when value changes
33 | );
34 | // Return enabled state and setter
35 | return [enabled, setEnabledState];
36 | }
37 |
38 | // Compose our useMedia hook to detect dark mode preference.
39 | // The API for useMedia looks a bit weird, but that's because ...
40 | // ... it was designed to support multiple media queries and return values.
41 | // Thanks to hook composition we can hide away that extra complexity!
42 | function usePrefersDarkMode() {
43 | return useMedia(["(prefers-color-scheme: dark)"], [true], false);
44 | }
45 |
46 | export default useDarkMode;
47 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
18 |
19 |
28 | React App
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/pages/Playlist.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import useFetch, { RequestStatus } from "../hooks/useFetch";
3 | import { axiosOptions, tracksUrl } from "../service/index";
4 | import Player from "../components/Player";
5 | import { AudioPlayerProvider } from "react-use-audio-player";
6 |
7 | interface Track {
8 | id: number;
9 | uri: string;
10 | title: string;
11 | index: number;
12 | artist: string;
13 | paused: boolean;
14 | played: boolean;
15 | playing: boolean;
16 | duration: number;
17 | percentage: number;
18 | stream_url: string;
19 | currentTime: number;
20 | artwork_url: string;
21 | permalink_url: string;
22 | favoritings_count: number;
23 | }
24 |
25 | function Playlist() {
26 | const { status, data, error, } = useFetch