├── app ├── favicon.ico ├── opengraph-image.png ├── robots.ts ├── (main) │ ├── watch │ │ ├── layout.tsx │ │ └── [...anime] │ │ │ └── page.tsx │ ├── details │ │ ├── layout.tsx │ │ └── [...anime] │ │ │ └── page.tsx │ ├── finished │ │ ├── layout.tsx │ │ └── page.tsx │ ├── ongoing │ │ ├── layout.tsx │ │ └── page.tsx │ ├── search │ │ ├── layout.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── schedule │ │ ├── layout.tsx │ │ ├── [day] │ │ │ └── page.tsx │ │ └── page.tsx │ ├── properties │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── [category] │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ └── page.tsx │ └── page.tsx ├── api │ ├── revalidate │ │ └── route.ts │ ├── properties │ │ ├── route.ts │ │ └── [category] │ │ │ ├── route.ts │ │ │ └── [id] │ │ │ └── route.ts │ ├── finished │ │ └── route.ts │ ├── ongoing │ │ └── route.ts │ ├── search │ │ └── route.ts │ ├── watch │ │ └── [...anime] │ │ │ └── route.ts │ ├── schedule │ │ ├── route.ts │ │ └── [day] │ │ │ └── route.ts │ └── details │ │ └── [...anime] │ │ └── route.ts ├── providers.tsx ├── sitemap.ts ├── globals.css └── layout.tsx ├── .vscode ├── extensions.json └── settings.json ├── .env.example ├── public ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── assets │ └── img │ │ ├── banner1.png │ │ └── sc-home-page.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── vercel.svg └── next.svg ├── lib ├── siteConfig.ts ├── getBaseUrl.ts ├── utils.ts ├── getDelay.ts ├── getOngoingAnime.ts ├── getFinishedAnime.ts ├── hooks │ ├── use-scroll.ts │ ├── use-local-storage.ts │ └── use-window-size.ts └── state-machine │ └── ToggleModeMachine.ts ├── postcss.config.js ├── .eslintrc.json ├── components ├── ui │ ├── aspect-ratio.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ ├── use-toast.ts │ ├── select.tsx │ ├── sheet.tsx │ └── toast.tsx ├── Sponsors.tsx ├── PropertiesNav.tsx ├── Search.tsx ├── ScheduleNav.tsx ├── Section.tsx ├── Footer.tsx ├── MainNav.tsx ├── RootLayoutGrid.tsx ├── Logo.tsx ├── Topbar.tsx └── Player.tsx ├── vercel.json ├── components.json ├── .gitignore ├── next.config.js ├── tsconfig.json ├── README.md ├── LICENSE ├── package.json └── tailwind.config.js /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["statelyai.stately-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeLens": true 4 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # domain name eg. https://example.com 2 | BASE_URL= 3 | 4 | # Optional 5 | UMAMI_TOKEN= -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/app/opengraph-image.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/img/banner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/assets/img/banner1.png -------------------------------------------------------------------------------- /lib/siteConfig.ts: -------------------------------------------------------------------------------- 1 | const siteConfig = { 2 | scraptUrl: "https://kuramanime.pro" 3 | } 4 | 5 | export default siteConfig -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/assets/img/sc-home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghikurniawan/nekomoe/HEAD/public/assets/img/sc-home-page.png -------------------------------------------------------------------------------- /lib/getBaseUrl.ts: -------------------------------------------------------------------------------- 1 | export const getBaseUrl = () => { 2 | const baseURL = process.env.BASE_URL || "http://localhost:3000" 3 | return baseURL 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "eslint:recommended"], 3 | "rules": { 4 | "@next/next/no-html-link-for-pages": ["error", "./app"] 5 | } 6 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | export default function robots() { 2 | return { 3 | rules: [ 4 | { 5 | userAgent: '*', 6 | }, 7 | ], 8 | sitemap: process.env.BASE_URL + '/sitemap.xml', 9 | host: process.env.BASE_URL, 10 | }; 11 | } -------------------------------------------------------------------------------- /lib/getDelay.ts: -------------------------------------------------------------------------------- 1 | export const getDelay = async (ms = 2000) => { 2 | const promise = await new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(true); 5 | }, ms); 6 | }).then((res) => { 7 | return res; 8 | }); 9 | return promise 10 | } -------------------------------------------------------------------------------- /app/(main)/watch/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import React from "react"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Watch", 6 | }; 7 | 8 | export default function LayoutWatch({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /app/(main)/details/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import React from "react"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Details", 6 | }; 7 | 8 | export default function DetailsLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /app/(main)/finished/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import React from "react"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Finished", 6 | }; 7 | 8 | export default function FinishedLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /app/(main)/ongoing/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import React from "react"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Ongoing", 6 | }; 7 | 8 | export default function PropertiesLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /lib/getOngoingAnime.ts: -------------------------------------------------------------------------------- 1 | import { getBaseUrl } from "./getBaseUrl"; 2 | 3 | export const getOngoingAnime = async (page: string | number = 1) => { 4 | const populars = await fetch(`${getBaseUrl()}/api/ongoing?page=${page}`, { 5 | headers: { "content-type": "aplication/json" }, 6 | }); 7 | const json = populars.json(); 8 | return json; 9 | }; -------------------------------------------------------------------------------- /lib/getFinishedAnime.ts: -------------------------------------------------------------------------------- 1 | import { getBaseUrl } from "./getBaseUrl"; 2 | 3 | export const getFinishedAnime = async (page: string | number = 1) => { 4 | const populars = await fetch(`${getBaseUrl()}/api/finished?page=${page}`, { 5 | headers: { "content-type": "aplication/json" }, 6 | }); 7 | const json = populars.json(); 8 | return json; 9 | }; -------------------------------------------------------------------------------- /app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { revalidatePath } from 'next/cache' 3 | 4 | export async function GET(request: NextRequest) { 5 | const path = request.nextUrl.searchParams.get('path') || '/' 6 | revalidatePath(path) 7 | return NextResponse.json({ revalidated: true, now: Date.now() }) 8 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "next build", 3 | "installCommand": "pnpm install", 4 | "redirects": [ 5 | { 6 | "source": "/analytics", 7 | "destination": "https://analytics.umami.is/share/hUOE7PeyHAnRzWwQ/nekomoe" 8 | }, 9 | { 10 | "source": "/github", 11 | "destination": "https://github.com/ghiwwwan/nekomoe" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "@/components/ui/toaster"; 4 | import { ThemeProvider } from "next-themes"; 5 | import React from "react"; 6 | 7 | export default function Providers({ children }: { children: React.ReactNode }) { 8 | return ( 9 | 10 | {children} 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/(main)/search/layout.tsx: -------------------------------------------------------------------------------- 1 | import Search from "@/components/Search"; 2 | import { FC, ReactNode } from "react"; 3 | import { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Search", 7 | }; 8 | 9 | const LayoutSearch: FC<{ children: ReactNode }> = ({ children }) => { 10 | return ( 11 | <> 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default LayoutSearch; 19 | -------------------------------------------------------------------------------- /app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import RootLayoutGrid from "@/components/RootLayoutGrid"; 2 | import SectionComponent from "@/components/Section"; 3 | import React, { FC } from "react"; 4 | 5 | interface MainLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const MainLayout: FC = ({ children }) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default MainLayout; 18 | -------------------------------------------------------------------------------- /app/(main)/schedule/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Metadata } from "next"; 3 | import ScheduleNav from "@/components/ScheduleNav"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Schedule", 7 | }; 8 | 9 | export default function ScheduleLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 |
17 | 18 |
19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(main)/properties/layout.tsx: -------------------------------------------------------------------------------- 1 | import PropertiesNav from "@/components/PropertiesNav"; 2 | import { Metadata } from "next"; 3 | import React from "react"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Properties", 7 | }; 8 | 9 | export default function LayoutProperties({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 |
17 | 18 |
19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/hooks/use-scroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export default function useScroll(threshold: number) { 4 | const [scrolled, setScrolled] = useState(false); 5 | 6 | const onScroll = useCallback(() => { 7 | setScrolled(window.pageYOffset > threshold); 8 | }, [threshold]); 9 | 10 | useEffect(() => { 11 | window.addEventListener("scroll", onScroll); 12 | return () => window.removeEventListener("scroll", onScroll); 13 | }, [onScroll]); 14 | 15 | return scrolled; 16 | } 17 | -------------------------------------------------------------------------------- /.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 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["cdn.myanimelist.net", "drive.google.com", "s4.anilist.co", "media.discordapp.net"] 5 | }, 6 | redirects: async () => { 7 | return [ 8 | { 9 | source: "/github", 10 | destination: "https://github.com/ghiwwwan/nekomoe", 11 | permanent: true, 12 | }, 13 | { 14 | source: "/analytics", 15 | destination: "https://analytics.umami.is/share/hUOE7PeyHAnRzWwQ/nekomoe", 16 | permanent: true, 17 | }, 18 | ] 19 | } 20 | } 21 | 22 | module.exports = nextConfig 23 | -------------------------------------------------------------------------------- /components/Sponsors.tsx: -------------------------------------------------------------------------------- 1 | const Sponsors = () => { 2 | return ( 3 |
4 |

5 | Nekomoe 6 |

7 |

8 | Nonton anime subtitle Indonesia Gratis tanpa Iklan! 9 |

10 |
11 | Sponsors 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default Sponsors; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /lib/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useLocalStorage = ( 4 | key: string, 5 | initialValue: T, 6 | // eslint-disable-next-line no-unused-vars 7 | ): [T, (value: T) => void] => { 8 | const [storedValue, setStoredValue] = useState(initialValue); 9 | 10 | useEffect(() => { 11 | // Retrieve from localStorage 12 | const item = window.localStorage.getItem(key); 13 | if (item) { 14 | setStoredValue(JSON.parse(item)); 15 | } 16 | }, [key]); 17 | 18 | const setValue = (value: T) => { 19 | // Save state 20 | setStoredValue(value); 21 | // Save to localStorage 22 | window.localStorage.setItem(key, JSON.stringify(value)); 23 | }; 24 | return [storedValue, setValue]; 25 | }; 26 | 27 | export default useLocalStorage; 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Nonton anime subtitle Indonesia Gratis! 3 |

Nekomoe

4 |
5 | 6 |

7 | Situs open-source buat nonton anime subtitle Indonesia Gratis! 8 |

9 | 10 |

11 | 12 | License 13 | 14 | Nekomoe GitHub repo 15 |

16 | 17 | 18 | Nonton anime subtitle Indonesia Gratis! 19 |

Nekomoe

20 |
21 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/PropertiesNav.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { buttonVariants } from "./ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const items = [ 6 | { title: "Genre" }, 7 | { title: "Season" }, 8 | { title: "Studio" }, 9 | { title: "Type" }, 10 | { title: "Quality" }, 11 | { title: "Source" }, 12 | { title: "Country" }, 13 | ]; 14 | 15 | const PropertiesNav = () => { 16 | return ( 17 |
    18 | {items.map((item) => ( 19 |
  • 20 | 21 | 27 | {item.title} 28 | 29 | 30 |
  • 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | export default PropertiesNav; 37 | -------------------------------------------------------------------------------- /lib/state-machine/ToggleModeMachine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createMachine, 3 | } from "xstate"; 4 | 5 | type TToggleModeMachine = "system" | "dark" | "light" 6 | 7 | export const ToggleModeMachine = createMachine({ 8 | /** @xstate-layout N4IgpgJg5mDOIC5QBcD2UoBswFtUTADpYBPWZXAYkwEsoALZAbQAYBdRUAB1VhuRqoAdpxAAPRAEYArABZCLaQHYAzJJYqAbACYW25QBoQJRLu2Ft6gJyyWSlrZV2Avs6NoM2PAUK0GySggAQwAnAGtWDiQQHj4BYVEJBBUVaUJJJSVJTU0rKwAOZUltWSMTBDMLa1t7Rxc3EA8sXHwiYPDKUnJcSNFY-kERaKSUtIysnLzCrJKyxHzJQlklK2zNSRVtbSsVAtcGoVb4aKavVr7eAYThxABaTTmEe9d3dGbvIi6KHAu4wcTELJtI98kpCHJMrJpNJNNIttolNIXo03mcfH5GL8rkNQEkSlZ0iwbNpppZERpHpVcisSppNiSlJp8sjTi0fO0wlj4jjxIDpItLPlppJZLJUiw7I8rCwFMslLJcrIMuohftnEA */ 9 | predictableActionArguments: true, 10 | schema: { 11 | events: {} as { type: TToggleModeMachine, value: TToggleModeMachine }, 12 | }, 13 | id: "togglemode", 14 | initial: "system", 15 | states: { 16 | system: { 17 | on: { 18 | light: "light", 19 | }, 20 | }, 21 | light: { 22 | on: { 23 | dark: "dark", 24 | } 25 | }, 26 | dark: { 27 | on: { 28 | system: "system", 29 | } 30 | } 31 | } 32 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Iwan Kurniawan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/hooks/use-window-size.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export default function useWindowSize() { 4 | const [windowSize, setWindowSize] = useState<{ 5 | width: number | undefined; 6 | height: number | undefined; 7 | }>({ 8 | width: undefined, 9 | height: undefined, 10 | }); 11 | 12 | useEffect(() => { 13 | // Handler to call on window resize 14 | function handleResize() { 15 | // Set window width/height to state 16 | setWindowSize({ 17 | width: window.innerWidth, 18 | height: window.innerHeight, 19 | }); 20 | } 21 | 22 | // Add event listener 23 | window.addEventListener("resize", handleResize); 24 | 25 | // Call handler right away so state gets updated with initial window size 26 | handleResize(); 27 | 28 | // Remove event listener on cleanup 29 | return () => window.removeEventListener("resize", handleResize); 30 | }, []); // Empty array ensures that effect is only run on mount 31 | 32 | return { 33 | windowSize, 34 | isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768, 35 | isDesktop: 36 | typeof windowSize?.width === "number" && windowSize?.width >= 768, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Input } from "@/components/ui/input"; 5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | 9 | const Search = () => { 10 | const [value, setValue] = useState(""); 11 | const router = useRouter(); 12 | 13 | const handleSearchSubmit = () => { 14 | if (value !== "") { 15 | router.push(`/search?query=${value}`); 16 | } 17 | }; 18 | 19 | return ( 20 |
21 | setValue(e.target.value)} 25 | onKeyDown={(e) => e.key == "Enter" && handleSearchSubmit()} 26 | placeholder="Search..." 27 | className="h-10 rounded-full focus-visible:ring-0" 28 | /> 29 |
30 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Search; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nekomoe", 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 | }, 11 | "dependencies": { 12 | "@radix-ui/react-aspect-ratio": "^1.0.3", 13 | "@radix-ui/react-dialog": "^1.0.4", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-scroll-area": "^1.0.4", 16 | "@radix-ui/react-select": "^1.2.2", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@radix-ui/react-toast": "^1.1.4", 19 | "@types/node": "20.4.2", 20 | "@types/react": "18.2.15", 21 | "@types/react-dom": "18.2.7", 22 | "@vercel/analytics": "^1.0.1", 23 | "@xstate/react": "^3.2.2", 24 | "autoprefixer": "10.4.14", 25 | "cheerio": "1.0.0-rc.12", 26 | "class-variance-authority": "^0.7.0", 27 | "clsx": "^2.0.0", 28 | "eslint": "8.45.0", 29 | "eslint-config-next": "13.4.11", 30 | "next": "13.4.11", 31 | "next-themes": "^0.2.1", 32 | "nextjs-toploader": "^1.4.2", 33 | "postcss": "8.4.27", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0", 36 | "react-player": "^2.12.0", 37 | "react-resizable-collapsible-grid": "^1.3.3", 38 | "sharp": "^0.32.4", 39 | "tailwind-merge": "^1.14.0", 40 | "tailwindcss": "3.3.3", 41 | "tailwindcss-animate": "^1.0.6", 42 | "typescript": "5.1.6", 43 | "xstate": "^4.38.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/ScheduleNav.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { buttonVariants } from "./ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const items = [ 6 | { 7 | title: "Senin", 8 | value: "monday", 9 | }, 10 | { 11 | title: "Selasa", 12 | value: "tuesday", 13 | }, 14 | { 15 | title: "Rabu", 16 | value: "wednesday", 17 | }, 18 | { 19 | title: "Kamis", 20 | value: "thursday", 21 | }, 22 | { 23 | title: "Jum'at", 24 | value: "friday", 25 | }, 26 | { 27 | title: "Sabtu", 28 | value: "saturday", 29 | }, 30 | { 31 | title: "Minggu", 32 | value: "sunday", 33 | }, 34 | ]; 35 | 36 | const ScheduleNav = () => { 37 | return ( 38 |
    39 |
  • 40 | 41 | 47 | All 48 | 49 | 50 |
  • 51 | {items.map((item) => ( 52 |
  • 53 | 54 | 60 | {item.title} 61 | 62 | 63 |
  • 64 | ))} 65 |
66 | ); 67 | }; 68 | 69 | export default ScheduleNav; 70 | -------------------------------------------------------------------------------- /app/api/properties/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | const cheerio = require('cheerio') 3 | import siteConfig from "@/lib/siteConfig"; 4 | const baseURL = siteConfig.scraptUrl 5 | 6 | export const runtime = "edge"; 7 | 8 | export async function GET(req: Request) { 9 | const params = new URL(req.url) 10 | const genre_type = params.searchParams.get('genre_type') 11 | const page = params.searchParams.get('page') 12 | try { 13 | const rawResponse = await fetch(`${baseURL}/properties/genre?genre_type=${genre_type || "all"}&page=${page || 1}`, { 14 | headers: { 15 | "User-Agent": 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 17 | "Accept-Language": "en-US,en;q=0.9", 18 | } 19 | }) 20 | const html = await rawResponse.text() 21 | const $ = cheerio.load(html); 22 | 23 | const element = $("#animeList > div > div > ul > li"); 24 | let datas: { genreName: any; genreId: any; }[] = []; 25 | 26 | element.each((i: any, e: any) => 27 | datas.push({ 28 | genreName: $(e).find(" a > span").text(), 29 | genreId: $(e) 30 | .find(" a > span") 31 | .text() 32 | .toLowerCase() 33 | .replace(/\s+/g, "-"), 34 | }) 35 | ); 36 | 37 | datas.pop() 38 | 39 | return NextResponse.json({ 40 | status: "success", 41 | statusCode: 200, 42 | properties: genre_type || "All", 43 | data: datas, 44 | }) 45 | } catch (err) { 46 | console.log(err) 47 | return NextResponse.json({ message: "Terjadi kesalahan saat mengambil data." }) 48 | } 49 | } -------------------------------------------------------------------------------- /app/(main)/properties/page.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/button"; 2 | import { getBaseUrl } from "@/lib/getBaseUrl"; 3 | import { cn } from "@/lib/utils"; 4 | import Link from "next/link"; 5 | import { Key, Suspense } from "react"; 6 | 7 | export default async function PropertiesPage() { 8 | return ( 9 |
10 |

Genre

11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | const getAllProperties = async () => { 19 | const properties = await fetch(`${getBaseUrl()}/api/properties`); 20 | const json = await properties?.json(); 21 | return json; 22 | }; 23 | 24 | const Properties = async () => { 25 | const properties = await getAllProperties(); 26 | if (properties?.message) { 27 | return ( 28 |
29 |

{properties?.message}

30 |
31 | ); 32 | } 33 | return ( 34 |
    35 | {properties?.data.map( 36 | (item: { 37 | genreId: Key | null | undefined; 38 | genreName: string | undefined; 39 | }) => ( 40 |
  • 41 | 42 | 48 | {item.genreName} 49 | 50 | 51 |
  • 52 | ) 53 | )} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /app/api/finished/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | const cheerio = require('cheerio') 3 | import siteConfig from "@/lib/siteConfig"; 4 | const baseURL = siteConfig.scraptUrl 5 | 6 | export const runtime = "edge"; 7 | 8 | export async function GET(req: Request) { 9 | const params = new URL(req.url) 10 | const order_by = params.searchParams.get('order_by') 11 | const page = params.searchParams.get('page') 12 | try { 13 | const rawResponse = await fetch(`${baseURL}/anime/finished?order_by=${order_by || "updated"}&page=${page || 1}`) 14 | const html = await rawResponse.text() 15 | const $ = cheerio.load(html); 16 | const element = $("#animeList > div > div"); 17 | let datas: { title: any; animeId: any; image: any; episode: any; }[] = []; 18 | 19 | element.each((i: any, e: any) => 20 | datas.push({ 21 | title: $(e).find("div > h5 > a").text(), 22 | animeId: $(e).find("div > h5 > a").attr("href") 23 | ? $(e) 24 | .find("div > h5 > a") 25 | .attr("href") 26 | .replace(`${baseURL}`, "") 27 | : "", 28 | image: $(e).find("a > div").attr("data-setbg"), 29 | episode: $(e) 30 | .find(" a > div > div.ep > span") 31 | .text() 32 | .replace(/\s+/g, " ") 33 | .trim(), 34 | }) 35 | ); 36 | 37 | datas.pop() 38 | 39 | return NextResponse.json({ 40 | status: "success", 41 | statusCode: 200, 42 | page: page || 1, 43 | order_by: order_by || "updated", 44 | data: datas, 45 | }) 46 | } catch (err) { 47 | console.log(err) 48 | return NextResponse.json({ message: "Terjadi kesalahan saat mengambil data." }) 49 | } 50 | } -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { getOngoingAnime } from "@/lib/getOngoingAnime"; 2 | 3 | export default async function sitemap() { 4 | 5 | const ongoing = [1, 2, 3, 4, 5, 6, 7].map((page) => ({ 6 | url: process.env.BASE_URL + '/ongoing?pages=' + page, 7 | lastModified: new Date().toISOString().split('T')[0], 8 | })); 9 | 10 | 11 | let o: any[] = []; 12 | async function fetchData() { 13 | await Promise.all( 14 | [1, 2, 3, 4, 5, 6, 7].map(async (p) => { 15 | const a = await getOngoingAnime(p); 16 | a?.data?.map((d: { animeId: string }) => { 17 | o.push({ 18 | url: new URL(process.env.BASE_URL + "/watch" + d.animeId).href, 19 | lastModified: new Date().toISOString().split("T")[0], 20 | }); 21 | }); 22 | }) 23 | ); 24 | return o; 25 | } 26 | 27 | const watchs = await fetchData(); 28 | 29 | let f: any[] = []; 30 | async function fetchFinished() { 31 | await Promise.all( 32 | [1, 2, 3, 4, 5, 6, 7].map(async (p) => { 33 | const a = await getOngoingAnime(p); 34 | a?.data?.map((d: { animeId: string }) => { 35 | f.push({ 36 | url: new URL(process.env.BASE_URL + "/details" + d.animeId).href, 37 | lastModified: new Date().toISOString().split("T")[0], 38 | }); 39 | }); 40 | }) 41 | ); 42 | return f; 43 | } 44 | 45 | const finished = await fetchFinished(); 46 | 47 | const routes = ['', '/ongoing', '/properties', '/finished'].map( 48 | (route) => ({ 49 | url: process.env.BASE_URL + route, 50 | lastModified: new Date().toISOString().split('T')[0], 51 | }) 52 | ); 53 | 54 | return [...routes, ...ongoing, ...watchs, ...finished]; 55 | } -------------------------------------------------------------------------------- /app/api/ongoing/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | const cheerio = require('cheerio') 3 | import siteConfig from "@/lib/siteConfig"; 4 | const baseURL = siteConfig.scraptUrl 5 | 6 | export const runtime = "edge"; 7 | 8 | export async function GET(req: Request) { 9 | const params = new URL(req.url) 10 | const order_by = params.searchParams.get('order_by') 11 | const page = params.searchParams.get('page') 12 | try { 13 | const rawResponse = await fetch(`${baseURL}/anime/ongoing?order_by=${order_by || "updated"}&page=${page || 1}`, { next: { revalidate: 60 * 60 } }) 14 | const html = await rawResponse.text() 15 | const $ = cheerio.load(html); 16 | const element = $("#animeList > div > div"); 17 | let datas: { title: any; animeId: any; image: any; episode: any; }[] = []; 18 | 19 | element.each((i: any, e: any) => 20 | datas.push({ 21 | title: $(e).find("div > h5 > a").text(), 22 | animeId: $(e).find("div > h5 > a").attr("href") 23 | ? $(e) 24 | .find("div > h5 > a") 25 | .attr("href") 26 | .replace(`${baseURL}`, "") 27 | : "", 28 | image: $(e).find("a > div").attr("data-setbg"), 29 | episode: $(e) 30 | .find(" a > div > div.ep > span") 31 | .text() 32 | .replace(/\s+/g, " ") 33 | .trim(), 34 | }) 35 | ); 36 | 37 | datas.pop() 38 | 39 | return NextResponse.json({ 40 | status: "success", 41 | statusCode: 200, 42 | page: page || 1, 43 | order_by: order_by || "updated", 44 | data: datas, 45 | }) 46 | } catch (err) { 47 | console.log(err) 48 | return NextResponse.json({ message: "Terjadi kesalahan saat mengambil data." }) 49 | } 50 | } -------------------------------------------------------------------------------- /components/Section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC, useState } from "react"; 4 | import * as ScrollArea from "@radix-ui/react-scroll-area"; 5 | import Topbar from "./Topbar"; 6 | 7 | const SectionComponent: FC<{ 8 | children: React.ReactNode; 9 | }> = ({ children }) => { 10 | const [scrolled, setScrolled] = useState(0); 11 | 12 | const handleScroll = (event: any) => { 13 | setScrolled(event.target.scrollTop); 14 | }; 15 | 16 | return ( 17 | 22 | 26 |
27 | 28 | {children} 29 |
30 |
31 | 35 | 36 | 37 | 38 |
39 | ); 40 | }; 41 | 42 | export default SectionComponent; 43 | -------------------------------------------------------------------------------- /app/api/properties/[category]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | const cheerio = require('cheerio') 3 | import siteConfig from "@/lib/siteConfig"; 4 | const baseURL = siteConfig.scraptUrl 5 | 6 | export const runtime = "edge"; 7 | 8 | export async function GET(req: Request, { params }: { params: { category: string } }) { 9 | const { category } = params 10 | const url = new URL(req.url) 11 | const order_by = url.searchParams.get('order_by') 12 | const page = url.searchParams.get('page') 13 | try { 14 | const rawResponse = await fetch(`${baseURL}/properties/${category}?order_by=${order_by || "ascending"}&page=${page || 1}`, { 15 | headers: { 16 | "User-Agent": 17 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 18 | "Accept-Language": "en-US,en;q=0.9", 19 | } 20 | }) 21 | const html = await rawResponse.text() 22 | const $ = cheerio.load(html); 23 | 24 | const element = $("#animeList > div > div > ul > li"); 25 | let datas: { genreName: any; genreId: any; }[] = []; 26 | 27 | element.each((i: any, e: any) => 28 | datas.push({ 29 | genreName: $(e).find(" a > span").text(), 30 | genreId: $(e) 31 | .find(" a > span") 32 | .text() 33 | .toLowerCase() 34 | .replace(/\s+/g, "-"), 35 | }) 36 | ); 37 | 38 | datas.pop() 39 | 40 | return NextResponse.json({ 41 | status: "success", 42 | statusCode: 200, 43 | category: category, 44 | // properties: req.query.genre_type || "genre", 45 | data: datas, 46 | }) 47 | } catch (err) { 48 | console.log(err) 49 | return NextResponse.json({ message: "Terjadi kesalahan saat mengambil data." }) 50 | } 51 | } -------------------------------------------------------------------------------- /app/(main)/properties/[category]/page.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/button"; 2 | import { getBaseUrl } from "@/lib/getBaseUrl"; 3 | import { cn } from "@/lib/utils"; 4 | import Link from "next/link"; 5 | 6 | import { Key, FC } from "react"; 7 | 8 | interface GenrePageProps { 9 | params: { category: string }; 10 | } 11 | const getAllCategory = async (category: string) => { 12 | const properties = await fetch(`${getBaseUrl()}/api/properties/${category}`); 13 | const json = await properties.json(); 14 | return json; 15 | }; 16 | const GenrePage: FC = async ({ params }) => { 17 | const { category } = params; 18 | const allCategory = await getAllCategory(category); 19 | if (allCategory?.message) { 20 | return ( 21 |
22 |

{allCategory?.message}

23 |
24 | ); 25 | } 26 | return ( 27 |
28 |

{category}

29 |
    30 | {allCategory?.data.map( 31 | ( 32 | item: { genreId: Key | null | undefined; genreName: string }, 33 | index: Key | null | undefined 34 | ) => ( 35 |
  • 36 | 37 | 43 | {item.genreName} 44 | 45 | 46 |
  • 47 | ) 48 | )} 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default GenrePage; 55 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: 240 5.9% 10%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | * { 40 | @apply border-border; 41 | } 42 | 43 | body { 44 | @apply bg-background text-foreground; 45 | } 46 | 47 | .dark { 48 | --background: 240 10% 3.9%; 49 | --foreground: 0 0% 98%; 50 | 51 | --muted: 240 3.7% 15.9%; 52 | --muted-foreground: 240 5% 64.9%; 53 | 54 | --popover: 240 10% 3.9%; 55 | --popover-foreground: 0 0% 98%; 56 | 57 | --card: 240 10% 3.9%; 58 | --card-foreground: 0 0% 98%; 59 | 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | 63 | --primary: 0 0% 98%; 64 | --primary-foreground: 240 5.9% 10%; 65 | 66 | --secondary: 240 3.7% 15.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | 69 | --accent: 240 3.7% 15.9%; 70 | --accent-foreground: 0 0% 98%; 71 | 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 85.7% 97.3%; 74 | 75 | --ring: 240 3.7% 15.9%; 76 | } 77 | } -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Logo from "@/components/Logo"; 2 | import { Button } from "@/components/ui/button"; 3 | import Link from "next/link"; 4 | import { EyeOpenIcon } from "@radix-ui/react-icons"; 5 | import { Suspense } from "react"; 6 | 7 | const UMAMI_TOKEN = process.env.UMAMI_TOKEN; 8 | 9 | const Footer = () => { 10 | return ( 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 | Nekomoe 19 |
20 | 21 |
22 |
23 | {UMAMI_TOKEN && ( 24 | 25 | 30 | 31 | )} 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Footer; 39 | 40 | const ActiveViews = async () => { 41 | const response = await fetch( 42 | "https://api.umami.is/v1/websites/a2f6a39d-27c9-4c55-9654-ebb1b3e73353/active", 43 | { 44 | headers: { 45 | Accept: "aplication/json", 46 | "x-umami-api-key": UMAMI_TOKEN as string, 47 | }, 48 | next: { revalidate: 60 }, 49 | } 50 | ); 51 | 52 | const active: { x: string }[] = await response.json(); 53 | 54 | return ( 55 | <> 56 | {active?.map((n) => n.x)} 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import Providers from "./providers"; 5 | import React from "react"; 6 | import NextTopLoader from "nextjs-toploader"; 7 | import Script from "next/script"; 8 | import { getBaseUrl } from "@/lib/getBaseUrl"; 9 | import Footer from "@/components/Footer"; 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | export const metadata: Metadata = { 14 | metadataBase: new URL(getBaseUrl()), 15 | title: { 16 | default: "Nekomoe", 17 | template: "%s | Nekomoe", 18 | }, 19 | description: "Nonton anime subtitle Indonesia Gratis tanpa Iklan!", 20 | openGraph: { 21 | title: "Nekomoe", 22 | description: "Nonton anime subtitle Indonesia Gratis tanpa Iklan!", 23 | url: process.env.BASE_URL, 24 | siteName: "Nekomoe", 25 | locale: "en-US", 26 | type: "website", 27 | }, 28 | robots: { 29 | index: true, 30 | follow: true, 31 | googleBot: { 32 | index: true, 33 | follow: true, 34 | "max-video-preview": -1, 35 | "max-image-preview": "large", 36 | "max-snippet": -1, 37 | }, 38 | }, 39 | twitter: { 40 | title: "Nekomoe", 41 | card: "summary_large_image", 42 | }, 43 | verification: { 44 | google: "3v0dsIPpvXYg0uLN3qF9I-1rOsCAcI1roNu2C0oPk64", 45 | yandex: "", 46 | }, 47 | }; 48 | 49 | export default function RootLayout({ 50 | children, 51 | }: { 52 | children: React.ReactNode; 53 | }) { 54 | return ( 55 | 56 | 57 | 58 | 59 | {children} 60 |