├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images │ ├── bg_image.jpg │ ├── bg_mobile.jpg │ ├── hero_desc.png │ ├── hero_title.png │ └── members │ │ ├── chaeyoung.jpg │ │ ├── dahyun.jpg │ │ ├── jeongyeon.jpg │ │ ├── jihyo.jpg │ │ ├── mina.jpg │ │ ├── momo.jpg │ │ ├── nayeon.jpg │ │ ├── sana.jpg │ │ └── tzuyu.jpg ├── logo.svg └── site.webmanifest ├── src ├── components │ ├── content │ │ ├── contentItem.tsx │ │ ├── index.ts │ │ └── latestContent.tsx │ ├── discography │ │ ├── discographyItem.tsx │ │ ├── discographySection.tsx │ │ ├── index.ts │ │ └── newestDiscographySection.tsx │ ├── footer │ │ └── index.tsx │ ├── header │ │ ├── index.tsx │ │ ├── navData.ts │ │ └── navLink.tsx │ ├── layout │ │ ├── container.tsx │ │ ├── index.ts │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── section.tsx │ ├── loadingScreen.tsx │ ├── memberItem.tsx │ ├── news │ │ ├── index.ts │ │ ├── latestNewsSection.tsx │ │ ├── newsItem.tsx │ │ └── newsSection.tsx │ ├── scrollDownAnimation.tsx │ └── verticalSocmedList.tsx ├── lib │ ├── developersPage.json │ ├── members.json │ ├── models │ │ ├── Contents.ts │ │ ├── Discography.ts │ │ ├── News.ts │ │ └── index.ts │ └── utils │ │ ├── clsx.ts │ │ ├── dbConnect.ts │ │ ├── opacityAnimation.ts │ │ └── useData.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── discography │ │ │ ├── [id].ts │ │ │ └── index.ts │ │ ├── members.ts │ │ └── news │ │ │ ├── [id].ts │ │ │ └── index.ts │ ├── developers.tsx │ ├── discography │ │ ├── [id].tsx │ │ └── index.tsx │ ├── index.tsx │ ├── news │ │ ├── [id].tsx │ │ └── index.tsx │ └── profile.tsx ├── styles │ ├── footer.module.css │ ├── globals.css │ └── header.module.css └── types │ ├── components.ts │ └── default.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URI= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["@typescript-eslint"], 10 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "prettier"], 11 | "rules": { 12 | "prefer-const": "error", 13 | "@typescript-eslint/no-unused-vars": "error", 14 | "@typescript-eslint/no-explicit-any": "error", 15 | "@typescript-eslint/no-empty-interface": [ 16 | "error", 17 | { 18 | "allowSingleExtends": false 19 | } 20 | ], 21 | "quotes": "off", 22 | "@typescript-eslint/quotes": ["error"], 23 | "no-extra-semi": "off", 24 | "@typescript-eslint/no-extra-semi": ["error"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TWICE Website from ONCE 2 | 3 | This website was created by ONCE. You can use the api used for this website at `/developers` 4 | 5 | ## Environment 6 | 7 | ```env 8 | MONGO_URI= 9 | ``` 10 | 11 | ## Run this website locally 12 | 13 | To run this project locally. 14 | 15 | ```bash 16 | # install dependencies 17 | npm install 18 | 19 | # serve with hot reload at localhost:3000 20 | npm run dev 21 | 22 | # build for production 23 | npm run build 24 | ``` 25 | -------------------------------------------------------------------------------- /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 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | skipLibCheck: true, 5 | images: { 6 | domains: ["i.ytimg.com", "lv2-cdn.azureedge.net", "s3-aop.plusmember.jp", "scontent.cdninstagram.com", "v.phinf.naver.net", "i0.wp.com"], 7 | }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-starter-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix" 11 | }, 12 | "dependencies": { 13 | "@tanem/react-nprogress": "^4.0.10", 14 | "dotenv": "^16.0.1", 15 | "mongoose": "^6.3.3", 16 | "next": "12.1.0", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "react-icons": "^4.3.1", 20 | "swr": "^1.3.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^15.14.1", 24 | "@types/react": "17.0.40", 25 | "@typescript-eslint/eslint-plugin": "^5.15.0", 26 | "@typescript-eslint/parser": "^5.15.0", 27 | "eslint": "8.11.0", 28 | "eslint-config-next": "12.1.0", 29 | "eslint-config-prettier": "^8.5.0", 30 | "prettier": "^2.6.0", 31 | "typescript": "^4.6.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/favicon.ico -------------------------------------------------------------------------------- /public/images/bg_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/bg_image.jpg -------------------------------------------------------------------------------- /public/images/bg_mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/bg_mobile.jpg -------------------------------------------------------------------------------- /public/images/hero_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/hero_desc.png -------------------------------------------------------------------------------- /public/images/hero_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/hero_title.png -------------------------------------------------------------------------------- /public/images/members/chaeyoung.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/chaeyoung.jpg -------------------------------------------------------------------------------- /public/images/members/dahyun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/dahyun.jpg -------------------------------------------------------------------------------- /public/images/members/jeongyeon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/jeongyeon.jpg -------------------------------------------------------------------------------- /public/images/members/jihyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/jihyo.jpg -------------------------------------------------------------------------------- /public/images/members/mina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/mina.jpg -------------------------------------------------------------------------------- /public/images/members/momo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/momo.jpg -------------------------------------------------------------------------------- /public/images/members/nayeon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/nayeon.jpg -------------------------------------------------------------------------------- /public/images/members/sana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/sana.jpg -------------------------------------------------------------------------------- /public/images/members/tzuyu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhafitf/twice/69074b68cd2a6c5e00cb1fed1b2e9f7f8c5096b8/public/images/members/tzuyu.jpg -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/components/content/contentItem.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { ContentItemProps } from "~types/components"; 3 | 4 | export default function ContentItem({ href, thumb, title, tag }: ContentItemProps) { 5 | return ( 6 | 7 |
8 |
9 | {title} 10 |
11 |
{tag}
12 |
13 |

{title}

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/content/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ContentItem } from "./contentItem"; 2 | export { default as LatestContent } from "./latestContent"; 3 | -------------------------------------------------------------------------------- /src/components/content/latestContent.tsx: -------------------------------------------------------------------------------- 1 | import ContentItem from "./contentItem"; 2 | import { ContentItemProps } from "~types/components"; 3 | 4 | interface IContentListProps { 5 | data: ContentItemProps[]; 6 | } 7 | 8 | export default function LatestContent({ data }: IContentListProps) { 9 | const renderItems = () => { 10 | if (data) { 11 | return data.map((item: ContentItemProps, index: number) => { 12 | return ; 13 | }); 14 | } 15 | }; 16 | 17 | return
{renderItems()}
; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/discography/discographyItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { DiscographyType } from "~types/components"; 3 | import Image from "next/image"; 4 | 5 | interface DiscographyItemProps { 6 | items?: DiscographyType; 7 | isSkeleton?: boolean; 8 | } 9 | 10 | export default function DiscographyItem({ items, isSkeleton }: DiscographyItemProps) { 11 | const renderItem = () => { 12 | if (items && !isSkeleton) { 13 | const { _id, type, name, releaseDate, coverArt } = items; 14 | return ( 15 | 16 | 17 |
18 | {name} 19 |
20 |
21 | {type} 22 |

{name}

23 |
{releaseDate} Release
24 |
25 |
26 | 27 | ); 28 | } 29 | return ( 30 |
31 |
32 |
33 | ); 34 | }; 35 | return <>{renderItem()}; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/discography/discographySection.tsx: -------------------------------------------------------------------------------- 1 | import useData from "~lib/utils/useData"; 2 | import DiscographyItem from "./discographyItem"; 3 | import { DiscographyType } from "~types/components"; 4 | 5 | export default function DiscographySection() { 6 | const { data } = useData("/api/discography"); 7 | const renderItems = () => { 8 | if (data) { 9 | return data.map((item: DiscographyType) => { 10 | return ; 11 | }); 12 | } 13 | 14 | return Array.from({ length: 6 }).map((_, index) => { 15 | return ; 16 | }); 17 | }; 18 | return
{renderItems()}
; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/discography/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DiscographyItem } from "./discographyItem"; 2 | export { default as DiscographySection } from "./discographySection"; 3 | export { default as NewestDiscographySection } from "./newestDiscographySection"; 4 | -------------------------------------------------------------------------------- /src/components/discography/newestDiscographySection.tsx: -------------------------------------------------------------------------------- 1 | import useData from "~lib/utils/useData"; 2 | import DiscographyItem from "./discographyItem"; 3 | import { DiscographyType } from "~types/components"; 4 | 5 | export default function NewestDiscographySection() { 6 | const { data } = useData("/api/discography"); 7 | const renderItems = () => { 8 | if (data) { 9 | return data.slice(0, 6).map((item: DiscographyType) => { 10 | return ; 11 | }); 12 | } 13 | 14 | return Array.from({ length: 6 }).map((_, index) => { 15 | return ; 16 | }); 17 | }; 18 | return
{renderItems()}
; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-target-blank */ 2 | import styles from "~styles/footer.module.css"; 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { Container } from "../layout"; 6 | 7 | export default function Footer() { 8 | return ( 9 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "~styles/header.module.css"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import NavLink from "./navLink"; 5 | import { left, right } from "./navData"; 6 | import { useEffect, useState } from "react"; 7 | import clsx from "~lib/utils/clsx"; 8 | import { useRouter } from "next/router"; 9 | import { addOpacity } from "~lib/utils/opacityAnimation"; 10 | 11 | export default function Header() { 12 | const [isFixed, setIsFixed] = useState(false); 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | const router = useRouter(); 16 | const burgerStyleNotHome = router.pathname === "/" ? {} : { background: "#434445" }; 17 | 18 | useEffect(() => { 19 | const handleScroll = () => { 20 | const scrollTop = window.pageYOffset; 21 | if (scrollTop > 70) { 22 | setIsFixed(true); 23 | } else { 24 | setIsFixed(false); 25 | } 26 | }; 27 | window.addEventListener("scroll", handleScroll); 28 | return () => { 29 | window.removeEventListener("scroll", handleScroll); 30 | }; 31 | }, []); 32 | 33 | useEffect(() => { 34 | isOpen ? document.body.classList.add("overflow-hidden") : document.body.classList.remove("overflow-hidden"); 35 | }, [isOpen]); 36 | 37 | let opacity = 0; 38 | 39 | const handleClickNav = () => { 40 | isOpen 41 | ? (opacity = 0) 42 | : addOpacity({ 43 | opacity: opacity, 44 | DOMByID: "nav", 45 | time: 10, 46 | }); 47 | }; 48 | 49 | return ( 50 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/header/navData.ts: -------------------------------------------------------------------------------- 1 | export const left = [ 2 | { href: "/", text: "Home" }, 3 | { href: "/news", text: "News" }, 4 | ]; 5 | 6 | export const right = [ 7 | { href: "/profile", text: "Profile" }, 8 | { href: "/discography", text: "Discography" }, 9 | ]; -------------------------------------------------------------------------------- /src/components/header/navLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { NavLinkProps } from "~types/components"; 3 | 4 | const NavLink = ({ className, href, text }: NavLinkProps) => { 5 | return ( 6 |
  • 7 | 8 | {text} 9 | 10 |
  • 11 | ); 12 | }; 13 | 14 | export default NavLink; 15 | -------------------------------------------------------------------------------- /src/components/layout/container.tsx: -------------------------------------------------------------------------------- 1 | import { ContainerProps } from "~types/components"; 2 | import clsx from "~lib/utils/clsx"; 3 | 4 | export default function Container({ children, className, style, isLarge, isSmall }: ContainerProps) { 5 | return ( 6 | <> 7 |
    8 | {children} 9 |
    10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Layout } from "./layout"; 2 | export { default as Container } from "./container"; 3 | export { default as Section } from "./section"; 4 | -------------------------------------------------------------------------------- /src/components/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { LayoutProps } from "~types/components"; 3 | import Footer from "../footer"; 4 | import Header from "../header"; 5 | 6 | export default function Layout(props: LayoutProps) { 7 | const { children, title, metaDescription, metaImage, style } = props; 8 | const image = metaImage || "/images/bg_image.jpg"; 9 | return ( 10 | <> 11 | 12 | {title} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 |
    26 |
    {children}
    27 |
    28 |
    29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/layout/loading.tsx: -------------------------------------------------------------------------------- 1 | import { useNProgress } from "@tanem/react-nprogress"; 2 | 3 | const Loading: React.FC<{ isRouteChanging: boolean }> = ({ isRouteChanging }) => { 4 | const { animationDuration, isFinished, progress } = useNProgress({ 5 | isAnimating: isRouteChanging, 6 | }); 7 | 8 | return ( 9 | <> 10 | 38 |
    39 |
    40 |
    41 |
    42 |
    43 | 44 | ); 45 | }; 46 | 47 | export default Loading; 48 | -------------------------------------------------------------------------------- /src/components/layout/section.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import clsx from "~lib/utils/clsx"; 3 | import { SectionProps } from "~types/components"; 4 | 5 | export default function Section({ title, children, className, isHomePage, withLoadMore }: SectionProps) { 6 | const href = title.toLowerCase().replace(/\s/g, "-"); 7 | return ( 8 | <> 9 |
    10 |
    11 |

    {title}

    12 | {withLoadMore && ( 13 | 14 | 15 | Load More 16 | 17 | 18 | )} 19 |
    20 | {children} 21 | {withLoadMore && ( 22 |
    23 | 24 | Load More 25 | 26 |
    27 | )} 28 |
    29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/loadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface LoadingScreenProps { 4 | isLoading: boolean; 5 | } 6 | 7 | export default function LoadingScreen({ isLoading }: LoadingScreenProps) { 8 | return ( 9 |
    10 |

    11 | Celebrate 12 |

    13 |

    14 | Celebrate 15 |

    16 |
    17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/memberItem.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { MemberItemProps } from "~/src/types/components"; 3 | 4 | interface Props extends MemberItemProps { 5 | onClick?: () => void; 6 | } 7 | 8 | export default function MemberItem({ onClick, image, name, nickname }: Props) { 9 | return ( 10 |
    11 |
    12 | {name} 13 |
    14 |
    15 |

    {nickname}

    16 |
    17 |
    18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/news/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NewsItem } from "./newsItem"; 2 | export { default as LatestNewsSection } from "./latestNewsSection"; 3 | export { default as NewsSection } from "./newsSection"; 4 | -------------------------------------------------------------------------------- /src/components/news/latestNewsSection.tsx: -------------------------------------------------------------------------------- 1 | import useData from "~lib/utils/useData"; 2 | import NewsItem from "./newsItem"; 3 | import { NewsType } from "~types/components"; 4 | 5 | export default function LatestNewsSection() { 6 | const { data } = useData("/api/news"); 7 | const renderItems = () => { 8 | if (data) { 9 | return data.slice(0, 6).map((item: NewsType) => { 10 | return ; 11 | }); 12 | } 13 | 14 | return Array.from({ length: 6 }).map((_, index) => { 15 | return ; 16 | }); 17 | }; 18 | return
    {renderItems()}
    ; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/news/newsItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { NewsType } from "~types/components"; 3 | 4 | interface NewsItemProps { 5 | items?: NewsType; 6 | isSkeleton?: boolean; 7 | } 8 | 9 | export default function NewsItem({ items, isSkeleton }: NewsItemProps) { 10 | const renderItem = () => { 11 | if (items && !isSkeleton) { 12 | const { _id, title, date } = items; 13 | return ( 14 | 15 | 16 |
    {date}
    17 |

    {title}

    18 |
    19 | 20 | ); 21 | } 22 | return ( 23 |
    24 |
    25 |

    26 |
    27 | ); 28 | }; 29 | return <>{renderItem()}; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/news/newsSection.tsx: -------------------------------------------------------------------------------- 1 | import useData from "~lib/utils/useData"; 2 | import NewsItem from "./newsItem"; 3 | import { NewsType } from "~types/components"; 4 | 5 | export default function LatestNewsSection() { 6 | const { data } = useData("/api/news"); 7 | const renderItems = () => { 8 | if (data) { 9 | return data.map((item: NewsType) => { 10 | return ; 11 | }); 12 | } 13 | 14 | return Array.from({ length: 6 }).map((_, index) => { 15 | return ; 16 | }); 17 | }; 18 | return
    {renderItems()}
    ; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/scrollDownAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import clsx from "~lib/utils/clsx"; 3 | 4 | export default function ScrollDownAnimation() { 5 | const [isFixed, setIsFixed] = useState(false); 6 | 7 | useEffect(() => { 8 | const handleScroll = () => { 9 | const scrollTop = window.pageYOffset; 10 | if (scrollTop > 70) { 11 | setIsFixed(true); 12 | } else { 13 | setIsFixed(false); 14 | } 15 | }; 16 | window.addEventListener("scroll", handleScroll); 17 | return () => { 18 | window.removeEventListener("scroll", handleScroll); 19 | }; 20 | }, []); 21 | 22 | return ( 23 | <> 24 |

    25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/verticalSocmedList.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineInstagram, AiOutlineTwitter } from "react-icons/ai"; 2 | import { BsYoutube } from "react-icons/bs"; 3 | import { FaTiktok } from "react-icons/fa"; 4 | 5 | interface LinksProps { 6 | href: string; 7 | icon: JSX.Element; 8 | ariaLabel: string; 9 | } 10 | 11 | const twiceKorea: LinksProps[] = [ 12 | { href: "https://www.instagram.com/twicetagram/", icon: , ariaLabel: "TWICE Korea's Instagram" }, 13 | { href: "https://twitter.com/jypetwice", icon: , ariaLabel: "TWICE Korea's Twitter" }, 14 | { href: "https://www.youtube.com/c/TWICE", icon: , ariaLabel: "TWICE Korea's Youtube" }, 15 | { href: "https://www.tiktok.com/@twice_tiktok_official", icon: , ariaLabel: "TWICE Korea's Tiktok" }, 16 | ]; 17 | 18 | const twiceJapan: LinksProps[] = [ 19 | { href: "https://www.instagram.com/jypetwice_japan/", icon: , ariaLabel: "TWICE Japan's Instagram" }, 20 | { href: "https://twitter.com/JYPETWICE_JAPAN", icon: , ariaLabel: "TWICE Japan's Twitter" }, 21 | { href: "https://www.youtube.com/channel/UCCRb6nYKaT8tzLA8CwDdUtw", icon: , ariaLabel: "TWICE Japan's Youtube" }, 22 | { href: "https://www.tiktok.com/@twice_tiktok_officialjp", icon: , ariaLabel: "TWICE Japan's TikTok" }, 23 | ]; 24 | 25 | const mappedList = (arrayElement: LinksProps[]) => { 26 | return arrayElement.map((item: LinksProps, index: number) => { 27 | return ( 28 |
  • 29 | 30 | {item.icon} 31 | 32 |
  • 33 | ); 34 | }); 35 | }; 36 | 37 | export default function VerticalSocmedList() { 38 | return ( 39 | <> 40 |
    41 |
      {mappedList(twiceKorea)}
    42 |

    TWICE Korea

    43 |
    44 |
    45 |
      {mappedList(twiceJapan)}
    46 |

    TWICE Japan

    47 |
    48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/developersPage.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Discography", 4 | "description": "Discography from TWICE", 5 | "apiReference": [ 6 | { 7 | "name": "List All Discography", 8 | "method": "get", 9 | "endpoint": "/api/discography", 10 | "response": [ 11 | { 12 | "_id": "number", 13 | "type": "string", 14 | "country": "string", 15 | "name": "string", 16 | "releaseDate": "string", 17 | "coverArt": "string", 18 | "content": "string", 19 | "tracks": ["string", "string"], 20 | "spotifyLinks": "string", 21 | "video": ["string", "string"] 22 | } 23 | ] 24 | }, 25 | { 26 | "name": "Discography Details", 27 | "method": "get", 28 | "endpoint": "/api/discography/:id", 29 | "params": [ 30 | { 31 | "name": "id", 32 | "type": "number", 33 | "description": "Discography id" 34 | } 35 | ], 36 | "response": { 37 | "_id": "number", 38 | "type": "string", 39 | "country": "string", 40 | "name": "string", 41 | "releaseDate": "string", 42 | "coverArt": "string", 43 | "content": "string", 44 | "tracks": ["string", "string"], 45 | "spotifyLinks": "string", 46 | "video": ["string", "string"] 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | "name": "Members", 53 | "description": "TWICE member list.", 54 | "apiReference": [ 55 | { 56 | "name": "List of Members", 57 | "method": "get", 58 | "endpoint": "/api/members", 59 | "response": { 60 | "name": "string", 61 | "nickname": "string", 62 | "born": "string", 63 | "bloodType": "string", 64 | "image": "string", 65 | "instagram": "string" 66 | } 67 | } 68 | ] 69 | }, 70 | { 71 | "name": "News", 72 | "description": "News list from TWICE", 73 | "apiReference": [ 74 | { 75 | "name": "List of News", 76 | "method": "get", 77 | "endpoint": "/api/news", 78 | "response": [ 79 | { 80 | "_id": "number", 81 | "title": "string", 82 | "date": "string", 83 | "htmlContent": "string" 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "News Details", 89 | "method": "get", 90 | "endpoint": "/api/news/:id", 91 | "params": [ 92 | { 93 | "name": "id", 94 | "type": "number", 95 | "description": "News id" 96 | } 97 | ], 98 | "response": { 99 | "_id": "number", 100 | "title": "string", 101 | "date": "string", 102 | "htmlContent": "string" 103 | } 104 | } 105 | ] 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /src/lib/members.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Im Nayeon", 4 | "nickname": "Nayeon", 5 | "born": "1995-09-22", 6 | "bloodType": "A", 7 | "image": "/images/members/nayeon.jpg", 8 | "instagram": "https://www.instagram.com/nayeonyny/" 9 | }, 10 | { 11 | "name": "Yoo Jeongyeon", 12 | "nickname": "Jeongyeon", 13 | "born": "1996-11-01", 14 | "bloodType": "O", 15 | "image": "/images/members/jeongyeon.jpg", 16 | "instagram": "https://www.instagram.com/jy_piece/" 17 | }, 18 | { 19 | "name": "Momo Hirai", 20 | "nickname": "Momo", 21 | "born": "1996-11-09", 22 | "bloodType": "A", 23 | "image": "/images/members/momo.jpg", 24 | "instagram": "https://www.instagram.com/momo/" 25 | }, 26 | { 27 | "name": "Sana Minatozaki", 28 | "nickname": "Sana", 29 | "born": "1996-12-29", 30 | "bloodType": "B", 31 | "image": "/images/members/sana.jpg", 32 | "instagram": "https://www.instagram.com/m.by__sana/" 33 | }, 34 | { 35 | "name": "Park Jihyo", 36 | "nickname": "Jihyo", 37 | "born": "1997-02-01", 38 | "bloodType": "O", 39 | "image": "/images/members/jihyo.jpg", 40 | "instagram": "https://www.instagram.com/_zyozyo/" 41 | }, 42 | { 43 | "name": "Mina Sharon Myoi", 44 | "nickname": "Mina", 45 | "born": "1997-03-24", 46 | "bloodType": "A", 47 | "image": "/images/members/mina.jpg", 48 | "instagram": "https://www.instagram.com/mina_sr_my/" 49 | }, 50 | { 51 | "name": "Kim Dahyun", 52 | "nickname": "Dahyun", 53 | "born": "1998-05-28", 54 | "bloodType": "O", 55 | "image": "/images/members/dahyun.jpg", 56 | "instagram": "https://www.instagram.com/dahhyunnee/" 57 | }, 58 | { 59 | "name": "Son Chaeyoung", 60 | "nickname": "Chaeyoung", 61 | "born": "1999-04-23", 62 | "bloodType": "B", 63 | "image": "/images/members/chaeyoung.jpg", 64 | "instagram": "https://www.instagram.com/chaeyo.0/" 65 | }, 66 | { 67 | "name": "Chou Tzuyu", 68 | "nickname": "Tzuyu", 69 | "born": "1999-06-14", 70 | "bloodType": "A", 71 | "image": "/images/members/tzuyu.jpg", 72 | "instagram": "https://www.instagram.com/thinkaboutzu/" 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /src/lib/models/Contents.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models, Model } from "mongoose"; 2 | import { ContentItemProps } from "~types/components"; 3 | 4 | const ContentSchema = new Schema({ 5 | tag: String, 6 | title: String, 7 | href: String, 8 | thumb: String, 9 | }); 10 | 11 | const Contents = (models.Contents as Model) || model("Contents", ContentSchema, "Contents"); 12 | export default Contents; 13 | -------------------------------------------------------------------------------- /src/lib/models/Discography.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const DiscographySchema = new Schema( 4 | { 5 | _id: Number, 6 | type: String, 7 | country: String, 8 | name: String, 9 | releaseDate: String, 10 | coverArt: String, 11 | tracks: Array, 12 | spotifyLink: String, 13 | }, 14 | { _id: false } 15 | ); 16 | 17 | const Discography = models.Discography || model("Discography", DiscographySchema, "Discography"); 18 | export default Discography; 19 | -------------------------------------------------------------------------------- /src/lib/models/News.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const NewsSchema = new Schema( 4 | { 5 | _id: Number, 6 | title: String, 7 | date: String, 8 | htmlContent: String, 9 | }, 10 | { _id: false } 11 | ); 12 | 13 | const News = models.News || model("News", NewsSchema, "News"); 14 | export default News; 15 | -------------------------------------------------------------------------------- /src/lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Discography } from "./Discography"; 2 | export { default as News } from "./News"; 3 | export { default as Contents } from "./Contents"; 4 | -------------------------------------------------------------------------------- /src/lib/utils/clsx.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export default function clsx(...classes: any) { 3 | return classes.filter(Boolean).join(" "); 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/utils/dbConnect.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const MONGODB_URI = `${process.env.MONGO_URI}`; 4 | 5 | if (!MONGODB_URI) { 6 | throw new Error("Please define the MONGODB_URI environment variable inside .env.local"); 7 | } 8 | 9 | /** 10 | * Global is used here to maintain a cached connection across hot reloads 11 | * in development. This prevents connections growing exponentially 12 | * during API Route usage. 13 | */ 14 | 15 | interface CustomNodeJsGlobal extends NodeJS.Global { 16 | mongoose: { 17 | conn: typeof mongoose | null; 18 | promise: Promise | null; 19 | }; 20 | } 21 | 22 | declare const global: CustomNodeJsGlobal; 23 | 24 | let cached = global.mongoose; 25 | 26 | if (!cached) { 27 | cached = global.mongoose = { conn: null, promise: null }; 28 | } 29 | 30 | export async function dbConnect() { 31 | if (cached.conn) { 32 | return cached.conn; 33 | } 34 | 35 | if (!cached.promise) { 36 | cached.promise = mongoose.connect(MONGODB_URI); 37 | } 38 | cached.conn = await cached.promise; 39 | return cached.conn; 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/utils/opacityAnimation.ts: -------------------------------------------------------------------------------- 1 | import { OpacityTransition } from "~types/components"; 2 | 3 | export const addOpacity = async ({ opacity, DOMByID, time }: OpacityTransition) => { 4 | if (opacity < 1) { 5 | opacity += 0.2; 6 | setTimeout(function () { 7 | addOpacity({ opacity, DOMByID, time }); 8 | }, time); 9 | } 10 | const DOMElement = document.getElementById(DOMByID); 11 | DOMElement && (DOMElement.style.opacity = `${opacity}`); 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/utils/useData.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export default function useData(endpoint: any) { 5 | const fetcher = async () => { 6 | const response = await fetch(endpoint); 7 | const data = await response.json(); 8 | return data; 9 | }; 10 | 11 | const { data, error } = useSWR(endpoint, fetcher); 12 | 13 | return { 14 | data, 15 | isLoading: !data && !error, 16 | isError: error, 17 | } as const; 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Layout } from "~components/layout"; 3 | 4 | export default function Custom404() { 5 | return ( 6 | 7 | 32 |
    33 |

    404 - Page Not Found

    34 | 35 | Back to Home 36 | 37 |
    38 |
    39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "~styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import Loading from "~components/layout/loading"; 4 | import { useRouter } from "next/router"; 5 | import { useEffect, useState } from "react"; 6 | 7 | const App: React.FC = ({ Component, pageProps }) => { 8 | const router = useRouter(); 9 | 10 | const [state, setState] = useState({ 11 | isRouteChanging: false, 12 | loadingKey: 0, 13 | }); 14 | 15 | useEffect(() => { 16 | const handleRouteChangeStart = () => { 17 | setState((prevState) => ({ 18 | ...prevState, 19 | isRouteChanging: true, 20 | loadingKey: prevState.loadingKey ^ 1, 21 | })); 22 | }; 23 | 24 | const handleRouteChangeEnd = () => { 25 | setState((prevState) => ({ 26 | ...prevState, 27 | isRouteChanging: false, 28 | })); 29 | }; 30 | router.events.on("routeChangeStart", handleRouteChangeStart); 31 | router.events.on("routeChangeComplete", handleRouteChangeEnd); 32 | router.events.on("routeChangeError", handleRouteChangeEnd); 33 | 34 | return () => { 35 | router.events.off("routeChangeStart", handleRouteChangeStart); 36 | router.events.off("routeChangeComplete", handleRouteChangeEnd); 37 | router.events.off("routeChangeError", handleRouteChangeEnd); 38 | }; 39 | }, [router.events]); 40 | 41 | return ( 42 | <> 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, Head, Html, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx: DocumentContext) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return initialProps; 7 | } 8 | render() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
    23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default MyDocument; 31 | -------------------------------------------------------------------------------- /src/pages/api/discography/[id].ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { dbConnect } from "~lib/utils/dbConnect"; 3 | import { Discography } from "~lib/models"; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method === "GET") { 7 | try { 8 | const id = req.query.id; 9 | 10 | await dbConnect(); 11 | const filter = { _id: id }; 12 | const discographyDB = await Discography.findOne(filter); 13 | return res.json(discographyDB); 14 | } catch (err: unknown) { 15 | return res.status(400).json({ message: "An error occured" }); 16 | } 17 | } 18 | 19 | return res.status(404).json({ message: "Not found" }); 20 | }; 21 | 22 | export default handler; 23 | -------------------------------------------------------------------------------- /src/pages/api/discography/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { dbConnect } from "~lib/utils/dbConnect"; 3 | import { Discography } from "~lib/models"; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method === "GET") { 7 | try { 8 | await dbConnect(); 9 | const discographyDB = await Discography.find().sort({ _id: -1 }); 10 | return res.json(discographyDB); 11 | } catch (err: unknown) { 12 | return res.status(400).json({ message: "An error occured" }); 13 | } 14 | } 15 | 16 | return res.status(404).json({ message: "Not found" }); 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /src/pages/api/members.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import members from "~lib/members.json"; 3 | 4 | const handler: NextApiHandler = async (req, res) => { 5 | if (req.method === "GET") { 6 | try { 7 | return res.status(200).json(members); 8 | } catch (err: unknown) { 9 | return res.status(400).json({ message: "An error occured" }); 10 | } 11 | } 12 | 13 | return res.status(404).json({ message: "Not found" }); 14 | }; 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /src/pages/api/news/[id].ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { dbConnect } from "~lib/utils/dbConnect"; 3 | import { News } from "~lib/models"; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method === "GET") { 7 | try { 8 | const id = req.query.id; 9 | 10 | await dbConnect(); 11 | const filter = { _id: id }; 12 | const newsDB = await News.findOne(filter); 13 | return res.json(newsDB); 14 | } catch (err: unknown) { 15 | return res.status(400).json({ message: "An error occured" }); 16 | } 17 | } 18 | 19 | return res.status(404).json({ message: "Not found" }); 20 | }; 21 | 22 | export default handler; 23 | -------------------------------------------------------------------------------- /src/pages/api/news/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { dbConnect } from "~lib/utils/dbConnect"; 3 | import { News } from "~lib/models"; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method === "GET") { 7 | try { 8 | await dbConnect(); 9 | const newsDB = await News.find().sort({ _id: -1 }); 10 | return res.json(newsDB); 11 | } catch (err: unknown) { 12 | return res.status(400).json({ message: "An error occured" }); 13 | } 14 | } 15 | 16 | return res.status(404).json({ message: "Not found" }); 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /src/pages/developers.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Container, Layout, Section } from "~components/layout"; 3 | import developersPage from "~lib/developersPage.json"; 4 | 5 | interface Props { 6 | name: string; 7 | description: string; 8 | apiReference: any; 9 | } 10 | 11 | const Items = ({ name, description, apiReference }: Props) => { 12 | return ( 13 |
    14 |

    {name}

    15 |

    {description}

    16 | {apiReference?.map((item: any, index: number) => { 17 | return ( 18 |
    19 |

    {item.name}

    20 |
    21 | {item.method} 22 | {item.endpoint} 23 |
    24 | {item.params && ( 25 |
    26 |

    Params

    27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {item.params.map((param: any, index: number) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | })} 45 | 46 |
    NameTypeDescription
    {param.name}{param.type}{param.description}
    47 |
    48 | )} 49 |
    50 |

    Response

    51 |
    52 |                 {JSON.stringify(item.response, null, 4).replace(/"/g, "")}
    53 |               
    54 |
    55 |
    56 | ); 57 | })} 58 |
    59 | ); 60 | }; 61 | 62 | const Developer = () => { 63 | return ( 64 | <> 65 | 66 | 67 |
    68 |

    69 | All media are obtained from{" "} 70 | 71 | JYPE 72 | 73 | {" & "} 74 | 75 | Sony Music 76 | 77 | . 78 |

    79 |
    80 | {developersPage.map((item, index) => { 81 | return ; 82 | })} 83 |
    84 |
    85 |
    86 |
    87 | 88 | ); 89 | }; 90 | 91 | export default Developer; 92 | -------------------------------------------------------------------------------- /src/pages/discography/[id].tsx: -------------------------------------------------------------------------------- 1 | import { DiscographyType } from "~types/components"; 2 | import { Container, Layout } from "~components/layout"; 3 | import Image from "next/image"; 4 | import useData from "~lib/utils/useData"; 5 | import { useRouter } from "next/router"; 6 | import Custom404 from "../404"; 7 | import LoadingScreen from "~components/loadingScreen"; 8 | 9 | const DiscographyByID = () => { 10 | const router = useRouter(); 11 | const { id } = router.query; 12 | const endpoint = id ? `/api/discography/${id}` : null; 13 | const { data, isLoading, isError } = useData(endpoint); 14 | 15 | const renderItems = () => { 16 | try { 17 | if (isLoading) { 18 | return ; 19 | } else if (data) { 20 | const { coverArt, name, releaseDate, type, tracks, video, content, spotifyLink }: DiscographyType = data; 21 | const spotifyID = spotifyLink.split("/").pop(); 22 | const youtubeID: string[] = []; 23 | 24 | if (video) { 25 | for (const item of video) { 26 | const idFromLink = item.split("=").pop(); 27 | youtubeID.push(`${idFromLink}`); 28 | } 29 | } 30 | 31 | const removedHTMLtag = content.replace(/<[^>]*>?/gm, ""); 32 | 33 | return ( 34 | 35 | 36 |
    37 |
    38 |
    39 | {name} 40 |
    41 |
    42 | {spotifyLink && ( 43 | 50 | )} 51 |
    52 |
    53 |
    54 |

    {name}

    55 |
    56 |
    {type}
    57 |
    {releaseDate} Release
    58 |
    59 |
    60 | {tracks.length > 0 && ( 61 |
    62 |

    Tracks Lists

    63 |
    64 | {tracks.map((track, index: number) => { 65 | const trackNumber = index + 1; 66 | return ( 67 |
    68 |
    {trackNumber.toString().padStart(2, "0")}.
    69 |
    {track}
    70 |
    71 | ); 72 | })} 73 |
    74 |
    75 | )} 76 |
    77 |
    78 |
    79 |

    Video

    80 | {youtubeID.map((idYoutube, index) => { 81 | return ( 82 |
    83 | 92 |
    93 | ); 94 | })} 95 |
    96 |
    97 |
    98 | ); 99 | } else if (isError) { 100 | throw new Error("Error"); 101 | } 102 | } catch (error) { 103 | return ; 104 | } 105 | }; 106 | 107 | return <>{renderItems()}; 108 | }; 109 | 110 | export default DiscographyByID; 111 | -------------------------------------------------------------------------------- /src/pages/discography/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { Layout, Container, Section } from "~components/layout"; 3 | import { DiscographySection } from "~components/discography"; 4 | 5 | const Discography: NextPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 |
    11 | 12 |
    13 |
    14 |
    15 | 16 | ); 17 | }; 18 | 19 | export default Discography; 20 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { Layout, Container, Section } from "~components/layout"; 3 | import VerticalSocmedList from "~components/verticalSocmedList"; 4 | import ScrollDownAnimation from "~components/scrollDownAnimation"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | import { useState } from "react"; 8 | import LoadingScreen from "~components/loadingScreen"; 9 | import { LatestContent } from "~components/content"; 10 | import { NewestDiscographySection } from "~components/discography"; 11 | import { LatestNewsSection } from "~components/news"; 12 | import Script from "next/script"; 13 | import { GetServerSideProps } from "next"; 14 | import { dbConnect } from "~lib/utils/dbConnect"; 15 | import Contents from "~/src/lib/models/Contents"; 16 | import { ContentItemProps } from "~types/components"; 17 | import { useEffect } from "react"; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | const Home: NextPage = (data: any) => { 21 | const [isLoading, setIsLoading] = useState(true); 22 | const [isLoader, setIsLoader] = useState(true); 23 | const metaDescription = 24 | "TWICE is a South Korean girl group formed by JYP Entertainment. The group is composed of nine members: Nayeon, Jeongyeon, Momo, Sana, Jihyo, Mina, Dahyun, Chaeyoung, and Tzuyu. Twice was formed under the television program Sixteen (2015) and debuted on October 20, 2015, with the extended play (EP) The Story Begins."; 25 | 26 | useEffect(() => { 27 | if (!isLoader) { 28 | setTimeout(function () { 29 | setIsLoading(false); 30 | }, 2000); 31 | } 32 | }, [isLoading, isLoader]); 33 | 34 | return ( 35 | <> 36 | {isLoading && } 37 | 38 |
    39 |
    40 |
    41 | 42 |
    43 |

    44 | Celebrate setIsLoader(false)} 54 | /> 55 |

    56 |
    57 | Celebrate Date 58 |
    59 |
    60 | 61 |
    62 |
    63 | 64 |
    65 | 66 |
    67 |
    68 | 69 |
    70 | 71 |
    72 |
    73 | 74 |
    75 | 76 |
    77 |
    78 |
    79 | 80 |
    About TWICE
    81 |

    82 | TWICE (Korean: 트와이스; RR: Teuwaiseu; Japanese: トゥワイス, Hepburn: To~uwaisu; commonly stylized in all caps as TWICE) is a South Korean girl group formed by JYP 83 | Entertainment. The group is composed of nine members: Nayeon, Jeongyeon, Momo, Sana, Jihyo, Mina, Dahyun, Chaeyoung, and Tzuyu. Twice was formed under the television 84 | program Sixteen (2015) and debuted on October 20, 2015, with the extended play (EP) The Story Begins.
    85 |
    The group debuted in Japan on June 28, 2017, under Warner Music Japan, with the release of a compilation album titled #Twice. The album charted at number 2 on 86 | the Oricon Albums Chart with the highest first-week album sales by a K-pop artist in Japan in two years. It was followed by the release of TWICE's first original 87 | Japanese maxi single titled "One More Time" in October. Twice became the first Korean girl group to earn a platinum certification from the Recording Industry 88 | Association of Japan (RIAJ) for both an album and CD single in the same year. Twice ranked third in the Top Artist category of Billboard Japan's 2017 Year-end 89 | Rankings, and in 2019, they became the first Korean girl group to embark on a Japanese dome tour. 90 |

    91 |
    92 | 93 | Members 94 | 95 |
    96 |
    97 |
    98 | 99 |