├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── components ├── authForm.tsx ├── gradientLayout.tsx ├── player.tsx ├── playerBar.tsx ├── playerLayout.tsx ├── sidebar.tsx └── songsTable.tsx ├── lib ├── auth.ts ├── fetcher.ts ├── formatters.ts ├── hooks.ts ├── mutations.ts ├── prisma.ts └── store.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _middleware.ts ├── api │ ├── me.ts │ ├── playlist.ts │ ├── signin.ts │ └── signup.ts ├── index.tsx ├── playlist │ └── [id].tsx ├── signin.tsx └── signup.tsx ├── prisma ├── migrations │ ├── 20211213203908_init │ │ └── migration.sql │ ├── 20211213213139_song_stuff │ │ └── migration.sql │ ├── 20211214202905_user_names │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma ├── seed.ts └── songsData.ts ├── public ├── favicon.ico ├── logo.svg └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["superjson-next"] 4 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['next/core-web-vitals', 'airbnb', 'airbnb/hooks', 'prettier'], 3 | plugins: ['react', '@typescript-eslint', 'prettier'], 4 | env: { 5 | browser: true, 6 | es2021: true, 7 | node: true, 8 | }, 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 13, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | 'react/react-in-jsx-scope': 'off', 19 | 'react/prop-types': 'off', 20 | 'react/jsx-props-no-spreading': 'off', 21 | 'import/prefer-default-export': 'off', 22 | 'no-param-reassign': 'off', 23 | 'import/extensions': [ 24 | 'error', 25 | 'ignorePackages', 26 | { 27 | ts: 'never', 28 | tsx: 'never', 29 | }, 30 | ], 31 | 'consistent-return': 'off', 32 | 'arrow-body-style': 'off', 33 | 'prefer-arrow-callback': 'off', 34 | 'react/jsx-filename-extension': 'off', 35 | 'react/function-component-definition': [ 36 | 'error', 37 | { 38 | namedComponents: 'arrow-function', 39 | unnamedComponents: 'arrow-function', 40 | }, 41 | ], 42 | 'prettier/prettier': 'warn', 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | .env 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/authForm.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Input, Button } from '@chakra-ui/react' 2 | import { useRouter } from 'next/router' 3 | import { FC, useState } from 'react' 4 | import { useSWRConfig } from 'swr' 5 | import NextImage from 'next/image' 6 | import { auth } from '../lib/mutations' 7 | 8 | const AuthForm: FC<{ mode: 'signin' | 'signup' }> = ({ mode }) => { 9 | const [email, setEmail] = useState('') 10 | const [password, setPassword] = useState('') 11 | const [isLoading, setIsLoading] = useState(false) 12 | const router = useRouter() 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault() 16 | setIsLoading(true) 17 | 18 | await auth(mode, { email, password }) 19 | setIsLoading(false) 20 | router.push('/') 21 | } 22 | 23 | return ( 24 | 25 | 31 | 32 | 33 | 34 | 35 |
36 | setEmail(e.target.value)} 40 | /> 41 | setPassword(e.target.value)} 45 | /> 46 | 58 |
59 |
60 |
61 |
62 | ) 63 | } 64 | 65 | export default AuthForm 66 | -------------------------------------------------------------------------------- /components/gradientLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from '@chakra-ui/layout' 2 | import { Image } from '@chakra-ui/react' 3 | 4 | const GradientLayout = ({ 5 | color, 6 | children, 7 | image, 8 | subtitle, 9 | title, 10 | description, 11 | roundImage, 12 | }) => { 13 | return ( 14 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | {subtitle} 31 | 32 | {title} 33 | {description} 34 | 35 | 36 | {children} 37 | 38 | ) 39 | } 40 | 41 | export default GradientLayout 42 | -------------------------------------------------------------------------------- /components/player.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonGroup, 3 | Box, 4 | IconButton, 5 | RangeSlider, 6 | RangeSliderFilledTrack, 7 | RangeSliderTrack, 8 | RangeSliderThumb, 9 | Center, 10 | Flex, 11 | Text, 12 | } from '@chakra-ui/react' 13 | import ReactHowler from 'react-howler' 14 | import { useEffect, useRef, useState } from 'react' 15 | import { 16 | MdShuffle, 17 | MdSkipPrevious, 18 | MdSkipNext, 19 | MdOutlinePlayCircleFilled, 20 | MdOutlinePauseCircleFilled, 21 | MdOutlineRepeat, 22 | } from 'react-icons/md' 23 | import { useStoreActions } from 'easy-peasy' 24 | import { formatTime } from '../lib/formatters' 25 | 26 | const Player = ({ songs, activeSong }) => { 27 | const [playing, setPlaying] = useState(true) 28 | const [index, setIndex] = useState( 29 | songs.findIndex((s) => s.id === activeSong.id) 30 | ) 31 | const [seek, setSeek] = useState(0.0) 32 | const [isSeeking, setIsSeeking] = useState(false) 33 | const [repeat, setRepeat] = useState(false) 34 | const [shuffle, setShuffle] = useState(false) 35 | const [duration, setDuration] = useState(0.0) 36 | const soundRef = useRef(null) 37 | const repeatRef = useRef(repeat) 38 | const setActiveSong = useStoreActions((state: any) => state.changeActiveSong) 39 | 40 | useEffect(() => { 41 | let timerId 42 | 43 | if (playing && !isSeeking) { 44 | const f = () => { 45 | setSeek(soundRef.current.seek()) 46 | timerId = requestAnimationFrame(f) 47 | } 48 | 49 | timerId = requestAnimationFrame(f) 50 | return () => cancelAnimationFrame(timerId) 51 | } 52 | 53 | cancelAnimationFrame(timerId) 54 | }, [playing, isSeeking]) 55 | 56 | useEffect(() => { 57 | setActiveSong(songs[index]) 58 | }, [index, setActiveSong, songs]) 59 | 60 | useEffect(() => { 61 | repeatRef.current = repeat 62 | }, [repeat]) 63 | 64 | const setPlayState = (value) => { 65 | setPlaying(value) 66 | } 67 | 68 | const onShuffle = () => { 69 | setShuffle((state) => !state) 70 | } 71 | 72 | const onRepeat = () => { 73 | setRepeat((state) => !state) 74 | } 75 | 76 | const prevSong = () => { 77 | setIndex((state) => { 78 | return state ? state - 1 : songs.length - 1 79 | }) 80 | } 81 | 82 | const nextSong = () => { 83 | setIndex((state) => { 84 | if (shuffle) { 85 | const next = Math.floor(Math.random() * songs.length) 86 | 87 | if (next === state) { 88 | return nextSong() 89 | } 90 | return next 91 | } 92 | 93 | return state === songs.length - 1 ? 0 : state + 1 94 | }) 95 | } 96 | 97 | const onEnd = () => { 98 | if (repeatRef.current) { 99 | setSeek(0) 100 | soundRef.current.seek(0) 101 | } else { 102 | nextSong() 103 | } 104 | } 105 | 106 | const onLoad = () => { 107 | const songDuration = soundRef.current.duration() 108 | setDuration(songDuration) 109 | } 110 | 111 | const onSeek = (e) => { 112 | setSeek(parseFloat(e[0])) 113 | soundRef.current.seek(e[0]) 114 | } 115 | 116 | return ( 117 | 118 | 119 | 126 | 127 |
128 | 129 | } 137 | /> 138 | } 144 | onClick={prevSong} 145 | /> 146 | {playing ? ( 147 | } 154 | onClick={() => setPlayState(false)} 155 | /> 156 | ) : ( 157 | } 164 | onClick={() => setPlayState(true)} 165 | /> 166 | )} 167 | 168 | } 174 | onClick={nextSong} 175 | /> 176 | } 184 | /> 185 | 186 |
187 | 188 | 189 | 190 | 191 | {formatTime(seek)} 192 | 193 | 194 | setIsSeeking(true)} 203 | onChangeEnd={() => setIsSeeking(false)} 204 | > 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | {formatTime(duration)} 213 | 214 | 215 | 216 |
217 | ) 218 | } 219 | 220 | export default Player 221 | -------------------------------------------------------------------------------- /components/playerBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from '@chakra-ui/layout' 2 | import { useStoreState } from 'easy-peasy' 3 | import Player from './player' 4 | 5 | const PlayerBar = () => { 6 | const songs = useStoreState((state: any) => state.activeSongs) 7 | const activeSong = useStoreState((state: any) => state.activeSong) 8 | 9 | return ( 10 | 11 | 12 | {activeSong ? ( 13 | 14 | {activeSong.name} 15 | {activeSong.artist.name} 16 | 17 | ) : null} 18 | 19 | {activeSong ? : null} 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | export default PlayerBar 27 | -------------------------------------------------------------------------------- /components/playerLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/layout' 2 | import Sidebar from './sidebar' 3 | import PlayerBar from './playerBar' 4 | 5 | const PlayerLayout = ({ children }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default PlayerLayout 22 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import NextImage from 'next/image' 2 | import NextLink from 'next/link' 3 | import { 4 | Box, 5 | List, 6 | ListItem, 7 | ListIcon, 8 | Divider, 9 | Center, 10 | LinkBox, 11 | LinkOverlay, 12 | } from '@chakra-ui/layout' 13 | import { 14 | MdHome, 15 | MdSearch, 16 | MdLibraryMusic, 17 | MdPlaylistAdd, 18 | MdFavorite, 19 | } from 'react-icons/md' 20 | import { usePlaylist } from '../lib/hooks' 21 | 22 | const navMenu = [ 23 | { 24 | name: 'Home', 25 | icon: MdHome, 26 | route: '/', 27 | }, 28 | { 29 | name: 'Search', 30 | icon: MdSearch, 31 | route: '/search', 32 | }, 33 | { 34 | name: 'Your Library', 35 | icon: MdLibraryMusic, 36 | route: '/library', 37 | }, 38 | ] 39 | 40 | const musicMenu = [ 41 | { 42 | name: 'Create Playlist', 43 | icon: MdPlaylistAdd, 44 | route: '/', 45 | }, 46 | { 47 | name: 'Favorites', 48 | icon: MdFavorite, 49 | route: '/favorites', 50 | }, 51 | ] 52 | 53 | // const playlists = new Array(30).fill(1).map((_, i) => `Playlist ${i + 1}`) 54 | 55 | const Sidebar = () => { 56 | const { playlists } = usePlaylist() 57 | return ( 58 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {navMenu.map((menu) => ( 72 | 73 | 74 | 75 | 76 | 81 | {menu.name} 82 | 83 | 84 | 85 | 86 | ))} 87 | 88 | 89 | 90 | 91 | {musicMenu.map((menu) => ( 92 | 93 | 94 | 95 | 96 | 101 | {menu.name} 102 | 103 | 104 | 105 | 106 | ))} 107 | 108 | 109 | 110 | 111 | 112 | {playlists.map((playlist) => ( 113 | 114 | 115 | 122 | {playlist.name} 123 | 124 | 125 | 126 | ))} 127 | 128 | 129 | 130 | 131 | ) 132 | } 133 | 134 | export default Sidebar 135 | -------------------------------------------------------------------------------- /components/songsTable.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/layout' 2 | import { Table, Thead, Td, Tr, Tbody, Th, IconButton } from '@chakra-ui/react' 3 | import { BsFillPlayFill } from 'react-icons/bs' 4 | import { AiOutlineClockCircle } from 'react-icons/ai' 5 | import { useStoreActions } from 'easy-peasy' 6 | import { formatDate, formatTime } from '../lib/formatters' 7 | 8 | const SongTable = ({ songs }) => { 9 | const playSongs = useStoreActions((store: any) => store.changeActiveSongs) 10 | const setActiveSong = useStoreActions((store: any) => store.changeActiveSong) 11 | 12 | const handlePlay = (activeSong?) => { 13 | setActiveSong(activeSong || songs[0]) 14 | playSongs(songs) 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | } 23 | aria-label="play" 24 | colorScheme="green" 25 | size="lg" 26 | isRound 27 | onClick={() => handlePlay()} 28 | /> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | {songs.map((song, i) => ( 43 | handlePlay(song)} 53 | > 54 | 55 | 56 | 57 | 58 | 59 | ))} 60 | 61 |
#TitleDate Added 37 | 38 |
{i + 1}{song.name}{formatDate(song.createdAt)}{formatTime(song.duration)}
62 |
63 |
64 | ) 65 | } 66 | 67 | export default SongTable 68 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | import prisma from './prisma' 4 | 5 | export const validateRoute = (handler) => { 6 | return async (req: NextApiRequest, res: NextApiResponse) => { 7 | const token = req.cookies.TRAX_ACCESS_TOKEN 8 | 9 | if (token) { 10 | let user 11 | 12 | try { 13 | const { id } = jwt.verify(token, 'hello') 14 | user = await prisma.user.findUnique({ 15 | where: { id }, 16 | }) 17 | 18 | if (!user) { 19 | throw new Error('Not real user') 20 | } 21 | } catch (error) { 22 | res.status(401) 23 | res.json({ error: 'Not Authorizied' }) 24 | return 25 | } 26 | 27 | return handler(req, res, user) 28 | } 29 | 30 | res.status(401) 31 | res.json({ error: 'Not Authorizied' }) 32 | } 33 | } 34 | 35 | export const validateToken = (token) => { 36 | const user = jwt.verify(token, 'hello') 37 | return user 38 | } 39 | -------------------------------------------------------------------------------- /lib/fetcher.ts: -------------------------------------------------------------------------------- 1 | export default function fetcher(url: string, data = undefined) { 2 | return fetch(`${window.location.origin}/api${url}`, { 3 | method: data ? 'POST' : 'GET', 4 | credentials: 'include', 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | body: JSON.stringify(data), 9 | }).then((res) => { 10 | if (res.status > 399 && res.status < 200) { 11 | throw new Error() 12 | } 13 | return res.json() 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /lib/formatters.ts: -------------------------------------------------------------------------------- 1 | import formatDuration from 'format-duration' 2 | 3 | export const formatTime = (timeInSeconds = 0) => { 4 | return formatDuration(timeInSeconds * 1000) 5 | } 6 | 7 | export const formatDate = (date: Date) => { 8 | return date.toLocaleDateString('en-US', { 9 | year: 'numeric', 10 | month: 'short', 11 | day: 'numeric', 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import fetcher from './fetcher' 3 | 4 | export const useMe = () => { 5 | const { data, error } = useSWR('/me', fetcher) 6 | 7 | return { 8 | user: data, 9 | isLoading: !data && !error, 10 | isError: error, 11 | } 12 | } 13 | 14 | export const usePlaylist = () => { 15 | const { data, error } = useSWR('/playlist', fetcher) 16 | return { 17 | playlists: (data as any) || [], 18 | isLoading: !data && !error, 19 | isError: error, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/mutations.ts: -------------------------------------------------------------------------------- 1 | import fetcher from './fetcher' 2 | 3 | export const auth = ( 4 | mode: 'signin' | 'signup', 5 | body: { email: string; password: string } 6 | ) => { 7 | return fetcher(`/${mode}`, body) 8 | } 9 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | export default new PrismaClient() 4 | -------------------------------------------------------------------------------- /lib/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, action } from 'easy-peasy' 2 | 3 | export const store = createStore({ 4 | activeSongs: [], 5 | activeSong: null, 6 | changeActiveSongs: action((state: any, payload) => { 7 | state.activeSongs = payload 8 | }), 9 | changeActiveSong: action((state: any, payload) => { 10 | state.activeSong = payload 11 | }), 12 | }) 13 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | typescript: { 4 | // !! WARN !! 5 | // Dangerously allow production builds to successfully complete even if 6 | // your project has type errors. 7 | // !! WARN !! 8 | ignoreBuildErrors: true, 9 | }, 10 | eslint: { 11 | // Warning: This allows production builds to successfully complete even if 12 | // your project has ESLint errors. 13 | ignoreDuringBuilds: true, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/layout": "^1.5.1", 12 | "@chakra-ui/react": "^1.7.2", 13 | "@dh-react-hooks/use-raf": "^0.9.1", 14 | "@emotion/react": "^11.6.0", 15 | "@emotion/styled": "^11.6.0", 16 | "@prisma/client": "^3.6.0", 17 | "bcrypt": "^5.0.1", 18 | "cookie": "^0.4.1", 19 | "cookies": "^0.8.0", 20 | "easy-peasy": "^5.0.4", 21 | "format-duration": "^1.4.0", 22 | "framer-motion": "^4.1.17", 23 | "jsonwebtoken": "^8.5.1", 24 | "next": "12.0.7", 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-howler": "^5.2.0", 28 | "react-icons": "^4.3.1", 29 | "reset-css": "^5.0.1", 30 | "swr": "^1.1.1" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "^17.0.37", 34 | "@typescript-eslint/eslint-plugin": "^5.6.0", 35 | "@typescript-eslint/parser": "^5.6.0", 36 | "babel-plugin-superjson-next": "^0.4.2", 37 | "eslint": "^8.4.1", 38 | "eslint-config-airbnb": "^19.0.2", 39 | "eslint-config-next": "12.0.7", 40 | "eslint-config-prettier": "^8.3.0", 41 | "eslint-plugin-import": "^2.25.3", 42 | "eslint-plugin-jsx-a11y": "^6.5.1", 43 | "eslint-plugin-prettier": "^4.0.0", 44 | "eslint-plugin-react": "^7.27.1", 45 | "eslint-plugin-react-hooks": "^4.3.0", 46 | "prettier": "^2.4.1", 47 | "prisma": "^3.6.0", 48 | "ts-node": "^10.4.0" 49 | }, 50 | "prisma": { 51 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider, extendTheme } from '@chakra-ui/react' 2 | import { StoreProvider } from 'easy-peasy' 3 | import PlayerLayout from '../components/playerLayout' 4 | import 'reset-css' 5 | import { store } from '../lib/store' 6 | 7 | const theme = extendTheme({ 8 | colors: { 9 | gray: { 10 | 100: '#F5f5f5', 11 | 200: '#EEEEEE', 12 | 300: '#E0E0E0', 13 | 400: '#BDBDBD', 14 | 500: '#9E9E9E', 15 | 600: '#757575', 16 | 700: '#616161', 17 | 800: '#424242', 18 | 900: '#212121', 19 | }, 20 | }, 21 | components: { 22 | Button: { 23 | variants: { 24 | link: { 25 | ':focus': { 26 | outline: 'none', 27 | boxShadow: 'none', 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | }) 34 | 35 | const MyApp = ({ Component, pageProps }) => { 36 | return ( 37 | 38 | 39 | {Component.authPage ? ( 40 | 41 | ) : ( 42 | 43 | 44 | 45 | )} 46 | 47 | 48 | ) 49 | } 50 | 51 | export default MyApp 52 | -------------------------------------------------------------------------------- /pages/_middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | const signedinPages = ['/', '/playlist', '/library'] 4 | 5 | export default function middleware(req) { 6 | if (signedinPages.find((p) => p === req.nextUrl.pathname)) { 7 | const token = req.cookies.TRAX_ACCESS_TOKEN 8 | 9 | if (!token) { 10 | return NextResponse.redirect('/signin') 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/me.ts: -------------------------------------------------------------------------------- 1 | import { validateRoute } from '../../lib/auth' 2 | import prisma from '../../lib/prisma' 3 | 4 | export default validateRoute(async (req, res, user) => { 5 | const playlistsCount = await prisma.playlist.count({ 6 | where: { 7 | userId: user.id, 8 | }, 9 | }) 10 | 11 | console.log(playlistsCount) 12 | res.json({ ...user, playlistsCount }) 13 | }) 14 | -------------------------------------------------------------------------------- /pages/api/playlist.ts: -------------------------------------------------------------------------------- 1 | import prisma from '../../lib/prisma' 2 | import { validateRoute } from '../../lib/auth' 3 | 4 | export default validateRoute(async (req, res, user) => { 5 | const playlists = await prisma.playlist.findMany({ 6 | where: { 7 | userId: user.id, 8 | }, 9 | orderBy: { 10 | name: 'asc', 11 | }, 12 | }) 13 | 14 | res.json(playlists) 15 | }) 16 | -------------------------------------------------------------------------------- /pages/api/signin.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import jwt from 'jsonwebtoken' 3 | import cookie from 'cookie' 4 | import { NextApiRequest, NextApiResponse } from 'next' 5 | import prisma from '../../lib/prisma' 6 | 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const { email, password } = req.body 9 | 10 | const user = await prisma.user.findUnique({ 11 | where: { 12 | email, 13 | }, 14 | }) 15 | 16 | if (user && bcrypt.compareSync(password, user.password)) { 17 | const token = jwt.sign( 18 | { 19 | id: user.id, 20 | email: user.email, 21 | time: Date.now(), 22 | }, 23 | 'hello', 24 | { 25 | expiresIn: '8h', 26 | } 27 | ) 28 | 29 | res.setHeader( 30 | 'Set-Cookie', 31 | cookie.serialize('TRAX_ACCESS_TOKEN', token, { 32 | httpOnly: true, 33 | maxAge: 8 * 60 * 60, 34 | path: '/', 35 | sameSite: 'lax', 36 | secure: process.env.NODE_ENV === 'production', 37 | }) 38 | ) 39 | 40 | res.json(user) 41 | } else { 42 | res.status(401) 43 | res.json({ error: 'Email or Password is wrong' }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/api/signup.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import jwt from 'jsonwebtoken' 3 | import cookie from 'cookie' 4 | import { NextApiRequest, NextApiResponse } from 'next' 5 | import prisma from '../../lib/prisma' 6 | 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const salt = bcrypt.genSaltSync() 9 | const { email, password } = req.body 10 | 11 | let user 12 | 13 | try { 14 | user = await prisma.user.create({ 15 | data: { 16 | email, 17 | password: bcrypt.hashSync(password, salt), 18 | }, 19 | }) 20 | } catch (e) { 21 | res.status(401) 22 | res.json({ error: 'User already exists' }) 23 | return 24 | } 25 | 26 | const token = jwt.sign( 27 | { 28 | email: user.email, 29 | id: user.id, 30 | time: Date.now(), 31 | }, 32 | 'hello', 33 | { expiresIn: '8h' } 34 | ) 35 | 36 | res.setHeader( 37 | 'Set-Cookie', 38 | cookie.serialize('TRAX_ACCESS_TOKEN', token, { 39 | httpOnly: true, 40 | maxAge: 8 * 60 * 60, 41 | path: '/', 42 | sameSite: 'lax', 43 | secure: process.env.NODE_ENV === 'production', 44 | }) 45 | ) 46 | 47 | res.json(user) 48 | } 49 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text, Flex } from '@chakra-ui/layout' 2 | import { Image } from '@chakra-ui/react' 3 | import GradientLayout from '../components/gradientLayout' 4 | import { useMe } from '../lib/hooks' 5 | import prisma from '../lib/prisma' 6 | 7 | const Home = ({ artists }) => { 8 | const { user } = useMe() 9 | 10 | return ( 11 | 19 | 20 | 21 | 22 | Top artist this month 23 | 24 | only visible to you 25 | 26 | 27 | {artists.map((artist) => ( 28 | 29 | 30 | 34 | 35 | {artist.name} 36 | Artist 37 | 38 | 39 | 40 | ))} 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | export const getServerSideProps = async () => { 48 | const artists = await prisma.artist.findMany({}) 49 | 50 | return { 51 | props: { artists }, 52 | } 53 | } 54 | 55 | export default Home 56 | -------------------------------------------------------------------------------- /pages/playlist/[id].tsx: -------------------------------------------------------------------------------- 1 | import GradientLayout from '../../components/gradientLayout' 2 | import SongTable from '../../components/songsTable' 3 | import { validateToken } from '../../lib/auth' 4 | import prisma from '../../lib/prisma' 5 | 6 | const getBGColor = (id) => { 7 | const colors = [ 8 | 'red', 9 | 'green', 10 | 'blue', 11 | 'orange', 12 | 'purple', 13 | 'gray', 14 | 'teal', 15 | 'yellow', 16 | ] 17 | 18 | return colors[id - 1] || colors[Math.floor(Math.random() * colors.length)] 19 | } 20 | 21 | const Playlist = ({ playlist }) => { 22 | const color = getBGColor(playlist.id) 23 | 24 | return ( 25 | 33 | 34 | 35 | ) 36 | } 37 | 38 | export const getServerSideProps = async ({ query, req }) => { 39 | let user 40 | 41 | try { 42 | user = validateToken(req.cookies.TRAX_ACCESS_TOKEN) 43 | } catch (e) { 44 | return { 45 | redirect: { 46 | permanent: false, 47 | destination: '/signin', 48 | }, 49 | } 50 | } 51 | 52 | const [playlist] = await prisma.playlist.findMany({ 53 | where: { 54 | id: +query.id, 55 | userId: user.id, 56 | }, 57 | include: { 58 | songs: { 59 | include: { 60 | artist: { 61 | select: { 62 | name: true, 63 | id: true, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }) 70 | 71 | return { 72 | props: { playlist }, 73 | } 74 | } 75 | export default Playlist 76 | -------------------------------------------------------------------------------- /pages/signin.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from '../components/authForm' 2 | 3 | const Signin = () => { 4 | return 5 | } 6 | 7 | Signin.authPage = true 8 | 9 | export default Signin 10 | -------------------------------------------------------------------------------- /pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from '../components/authForm' 2 | 3 | const Signup = () => { 4 | return 5 | } 6 | 7 | Signup.authPage = true 8 | 9 | export default Signup 10 | -------------------------------------------------------------------------------- /prisma/migrations/20211213203908_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "email" TEXT NOT NULL, 7 | "password" TEXT NOT NULL, 8 | 9 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "Song" ( 14 | "id" SERIAL NOT NULL, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP(3) NOT NULL, 17 | "name" TEXT NOT NULL, 18 | "artistId" INTEGER NOT NULL, 19 | 20 | CONSTRAINT "Song_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- CreateTable 24 | CREATE TABLE "Artist" ( 25 | "id" SERIAL NOT NULL, 26 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | "updatedAt" TIMESTAMP(3) NOT NULL, 28 | "name" TEXT NOT NULL, 29 | 30 | CONSTRAINT "Artist_pkey" PRIMARY KEY ("id") 31 | ); 32 | 33 | -- CreateTable 34 | CREATE TABLE "Playlist" ( 35 | "id" SERIAL NOT NULL, 36 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 37 | "updatedAt" TIMESTAMP(3) NOT NULL, 38 | "name" TEXT NOT NULL, 39 | "userId" INTEGER, 40 | 41 | CONSTRAINT "Playlist_pkey" PRIMARY KEY ("id") 42 | ); 43 | 44 | -- CreateTable 45 | CREATE TABLE "_PlaylistToSong" ( 46 | "A" INTEGER NOT NULL, 47 | "B" INTEGER NOT NULL 48 | ); 49 | 50 | -- CreateIndex 51 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 52 | 53 | -- CreateIndex 54 | CREATE UNIQUE INDEX "Artist_name_key" ON "Artist"("name"); 55 | 56 | -- CreateIndex 57 | CREATE UNIQUE INDEX "_PlaylistToSong_AB_unique" ON "_PlaylistToSong"("A", "B"); 58 | 59 | -- CreateIndex 60 | CREATE INDEX "_PlaylistToSong_B_index" ON "_PlaylistToSong"("B"); 61 | 62 | -- AddForeignKey 63 | ALTER TABLE "Song" ADD CONSTRAINT "Song_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 64 | 65 | -- AddForeignKey 66 | ALTER TABLE "Playlist" ADD CONSTRAINT "Playlist_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 67 | 68 | -- AddForeignKey 69 | ALTER TABLE "_PlaylistToSong" ADD FOREIGN KEY ("A") REFERENCES "Playlist"("id") ON DELETE CASCADE ON UPDATE CASCADE; 70 | 71 | -- AddForeignKey 72 | ALTER TABLE "_PlaylistToSong" ADD FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE; 73 | -------------------------------------------------------------------------------- /prisma/migrations/20211213213139_song_stuff/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `userId` on table `Playlist` required. This step will fail if there are existing NULL values in that column. 5 | - Added the required column `duration` to the `Song` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `url` to the `Song` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- DropForeignKey 10 | ALTER TABLE "Playlist" DROP CONSTRAINT "Playlist_userId_fkey"; 11 | 12 | -- AlterTable 13 | ALTER TABLE "Playlist" ALTER COLUMN "userId" SET NOT NULL; 14 | 15 | -- AlterTable 16 | ALTER TABLE "Song" ADD COLUMN "duration" INTEGER NOT NULL, 17 | ADD COLUMN "url" TEXT NOT NULL; 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "Playlist" ADD CONSTRAINT "Playlist_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /prisma/migrations/20211214202905_user_names/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `firstName` to the `User` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `lastName` to the `User` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "User" ADD COLUMN "firstName" TEXT NOT NULL, 10 | ADD COLUMN "lastName" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL") 12 | } 13 | 14 | model User { 15 | id Int @id @default(autoincrement()) 16 | createdAt DateTime @default(now()) 17 | updatedAt DateTime @updatedAt 18 | email String @unique 19 | firstName String 20 | lastName String 21 | password String 22 | playlists Playlist[] 23 | } 24 | 25 | model Song { 26 | id Int @id @default(autoincrement()) 27 | createdAt DateTime @default(now()) 28 | updatedAt DateTime @updatedAt 29 | name String 30 | artist Artist @relation(fields: [artistId], references: [id]) 31 | artistId Int 32 | playlists Playlist[] 33 | duration Int 34 | url String 35 | } 36 | 37 | model Artist { 38 | id Int @id @default(autoincrement()) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | songs Song[] 42 | name String @unique 43 | } 44 | 45 | model Playlist { 46 | id Int @id @default(autoincrement()) 47 | createdAt DateTime @default(now()) 48 | updatedAt DateTime @updatedAt 49 | name String 50 | songs Song[] 51 | user User @relation(fields: [userId], references: [id]) 52 | userId Int 53 | } 54 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import bcrypt from 'bcrypt' 3 | import { artistsData } from './songsData' 4 | 5 | const prisma = new PrismaClient() 6 | 7 | const run = async () => { 8 | await Promise.all( 9 | artistsData.map(async (artist) => { 10 | return prisma.artist.upsert({ 11 | where: { name: artist.name }, 12 | update: {}, 13 | create: { 14 | name: artist.name, 15 | songs: { 16 | create: artist.songs.map((song) => ({ 17 | name: song.name, 18 | duration: song.duration, 19 | url: song.url, 20 | })), 21 | }, 22 | }, 23 | }) 24 | }) 25 | ) 26 | 27 | const salt = bcrypt.genSaltSync() 28 | const user = await prisma.user.upsert({ 29 | where: { email: 'user@test.com' }, 30 | update: {}, 31 | create: { 32 | email: 'user@test.com', 33 | password: bcrypt.hashSync('password', salt), 34 | firstName: 'Scott', 35 | lastName: 'Moss', 36 | }, 37 | }) 38 | 39 | const songs = await prisma.song.findMany({}) 40 | await Promise.all( 41 | new Array(10).fill(1).map(async (_, i) => { 42 | return prisma.playlist.create({ 43 | data: { 44 | name: `Playlist #${i + 1}`, 45 | user: { 46 | connect: { id: user.id }, 47 | }, 48 | songs: { 49 | connect: songs.map((song) => ({ 50 | id: song.id, 51 | })), 52 | }, 53 | }, 54 | }) 55 | }) 56 | ) 57 | } 58 | 59 | run() 60 | .catch((e) => { 61 | console.error(e) 62 | process.exit(1) 63 | }) 64 | .finally(async () => { 65 | await prisma.$disconnect() 66 | }) 67 | -------------------------------------------------------------------------------- /prisma/songsData.ts: -------------------------------------------------------------------------------- 1 | export const artistsData: { 2 | name: string 3 | songs: any[] 4 | }[] = [ 5 | { 6 | name: 'Glitch', 7 | songs: [ 8 | { 9 | name: 'Fermi Paradox', 10 | duration: 235, 11 | 12 | url: 'https://dl.dropboxusercontent.com/s/7xmpwvvek6szx5n/fermi-paradox.mp3?dl=0', 13 | }, 14 | ], 15 | }, 16 | { 17 | name: 'Purple Cat', 18 | songs: [ 19 | { 20 | name: 'Long Day', 21 | duration: 185, 22 | url: 'https://dl.dropboxusercontent.com/s/9h90r7ku3df5o9y/long-day.mp3?dl=0', 23 | }, 24 | ], 25 | }, 26 | { 27 | name: 'Ben Sound', 28 | songs: [ 29 | { 30 | name: 'The Elevator Bossa Nova', 31 | duration: 238, 32 | url: 'https://dl.dropboxusercontent.com/s/7dh5o3kfjcz0nh3/The-Elevator-Bossa-Nova.mp3?dl=0', 33 | }, 34 | ], 35 | }, 36 | { 37 | name: 'LiQWYD', 38 | songs: [ 39 | { 40 | name: 'Winter', 41 | duration: 162, 42 | url: 'https://dl.dropboxusercontent.com/s/tlx2zev0as500ki/winter.mp3?dl=0', 43 | }, 44 | ], 45 | }, 46 | { 47 | name: 'FSM Team', 48 | songs: [ 49 | { 50 | name: 'Eternal Springtime', 51 | duration: 302, 52 | url: 'https://dl.dropboxusercontent.com/s/92u8d427bz0b1t8/eternal-springtime.mp3?dl=0', 53 | }, 54 | { 55 | name: 'Astronaut in a Submarine', 56 | duration: 239, 57 | artist: 'FSM Team', 58 | url: 'https://dl.dropboxusercontent.com/s/9b43fr6epbgji4f/astronaut-in-a-submarine.mp3?dl=0', 59 | }, 60 | ], 61 | }, 62 | ] 63 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/fullstack-music/1722a43dfc1062d05a63152dec9e76e9d9762645/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /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 | "strict": false, 12 | "downlevelIteration": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } --------------------------------------------------------------------------------