├── src
├── app
│ ├── favicon.ico
│ ├── ReactQueryProvider.tsx
│ ├── layout.tsx
│ ├── globals.css
│ └── page.tsx
├── lib
│ └── utils.ts
└── components
│ └── ui
│ └── image-card.tsx
├── public
├── vercel.svg
├── window.svg
├── file.svg
├── globe.svg
└── next.svg
├── postcss.config.mjs
├── next-env.d.ts
├── next.config.ts
├── eslint.config.mjs
├── components.json
├── .gitignore
├── tsconfig.json
├── package.json
├── tailwind.config.ts
└── README.md
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animemoeus/waifu-animemoeus/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | images: {
6 | remotePatterns: [
7 | {
8 | protocol: 'https',
9 | hostname: 'media.discordapp.net',
10 | // port: '',
11 | // pathname: '/account123/**',
12 | // search: '',
13 | },
14 | ],
15 | },
16 | };
17 |
18 | export default nextConfig;
19 |
--------------------------------------------------------------------------------
/src/app/ReactQueryProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 | import { useState } from "react";
5 |
6 | export default function ReactQueryProvider({ children }: { children: React.ReactNode }) {
7 | const [queryClient] = useState(() => new QueryClient()); // Persist Query Client
8 |
9 | return {children};
10 | }
11 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/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.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 | import ReactQueryProvider from "./ReactQueryProvider";
5 |
6 | const geistSans = Geist({
7 | variable: "--font-geist-sans",
8 | subsets: ["latin"],
9 | });
10 |
11 | const geistMono = Geist_Mono({
12 | variable: "--font-geist-mono",
13 | subsets: ["latin"],
14 | });
15 |
16 | export const metadata: Metadata = {
17 | title: "Waifu AnimeMoeUs",
18 | description: "Waifu AnimeMoeUs, Arter Tendean",
19 | };
20 |
21 | export default function RootLayout({
22 | children,
23 | }: Readonly<{ children: React.ReactNode }>) {
24 | return (
25 |
26 |
29 | {children}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "waifu-animemoeus",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@tanstack/react-query": "^5.68.0",
13 | "class-variance-authority": "^0.7.1",
14 | "clsx": "^2.1.1",
15 | "lucide-react": "^0.482.0",
16 | "next": "15.1.7",
17 | "react": "^19.0.0",
18 | "react-dom": "^19.0.0",
19 | "react-infinite-scroll-component": "^6.1.0",
20 | "react-masonry-css": "^1.0.16",
21 | "tailwind-merge": "^3.0.2",
22 | "tailwindcss-animate": "^1.0.7"
23 | },
24 | "devDependencies": {
25 | "@eslint/eslintrc": "^3",
26 | "@types/node": "^20",
27 | "@types/react": "^19",
28 | "@types/react-dom": "^19",
29 | "eslint": "^9",
30 | "eslint-config-next": "15.1.7",
31 | "postcss": "^8",
32 | "prettier": "^3.5.3",
33 | "tailwindcss": "^3.4.1",
34 | "typescript": "^5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | main: 'var(--main)',
14 | overlay: 'var(--overlay)',
15 | bg: 'var(--bg)',
16 | bw: 'var(--bw)',
17 | blank: 'var(--blank)',
18 | text: 'var(--text)',
19 | mtext: 'var(--mtext)',
20 | border: 'var(--border)',
21 | ring: 'var(--ring)',
22 | ringOffset: 'var(--ring-offset)',
23 |
24 | secondaryBlack: '#212121',
25 | },
26 | borderRadius: {
27 | base: '5px'
28 | },
29 | boxShadow: {
30 | shadow: 'var(--shadow)'
31 | },
32 | translate: {
33 | boxShadowX: '4px',
34 | boxShadowY: '4px',
35 | reverseBoxShadowX: '-4px',
36 | reverseBoxShadowY: '-4px',
37 | },
38 | fontWeight: {
39 | base: '500',
40 | heading: '700',
41 | },
42 | },
43 | },
44 | plugins: [require("tailwindcss-animate")],
45 | } satisfies Config;
46 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ui/image-card.tsx:
--------------------------------------------------------------------------------
1 | import { IWaifu } from '@/app/page';
2 | import Image from 'next/image';
3 |
4 | type Props = {
5 | imageUrl: string;
6 | caption: string;
7 | width?: number;
8 | height?: number;
9 | blurDataURL?: string;
10 | raw: IWaifu;
11 | }
12 |
13 | export default function ImageCard({
14 | imageUrl,
15 | caption,
16 | width = 250,
17 | height = 188,
18 | blurDataURL,
19 | raw
20 | }: Props) {
21 | return (
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 | {raw.creator_name}
38 |
39 |
{raw.caption}
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --main: #FFDC58;
7 | --overlay: rgba(0, 0, 0, 0.8);
8 | /* background color overlay for alert dialogs, modals, etc. */
9 |
10 | --bg: #FEF2E8;
11 | --bw: #fff;
12 | /* white and secondary black e.g. utility class variant would be: bg-[#fff] dark:bg-[#212121] */
13 | --blank: #000;
14 | /* blank black and white */
15 | --border: #000;
16 | --text: #000;
17 | --mtext: #000;
18 | /* text that is placed on background with main color e.g. text color of default button */
19 | --ring: #000;
20 | --ring-offset: #fff;
21 |
22 | --border-radius: 5px;
23 | --box-shadow-x: 4px;
24 | --box-shadow-y: 4px;
25 | --reverse-box-shadow-x: -4px;
26 | --reverse-box-shadow-y: -4px;
27 | --base-font-weight: 500;
28 | --heading-font-weight: 700;
29 |
30 | --shadow: var(--box-shadow-x) var(--box-shadow-y) 0px 0px var(--border);
31 | }
32 |
33 | .dark {
34 | --bg: #374151;
35 | --bw: #212121;
36 | --blank: #fff;
37 | --border: #000;
38 | --text: #e6e6e6;
39 | --mtext: #000;
40 | --ring: #fff;
41 | --ring-offset: #000;
42 |
43 | --shadow: var(--box-shadow-x) var(--box-shadow-y) 0px 0px var(--border);
44 | }
45 |
46 | .my-masonry-grid {
47 | display: -webkit-box;
48 | /* Not needed if autoprefixing */
49 | display: -ms-flexbox;
50 | /* Not needed if autoprefixing */
51 | display: flex;
52 | margin-left: -10px;
53 | /* gutter size offset */
54 | width: auto;
55 | }
56 |
57 | .my-masonry-grid_column {
58 | padding-left: 10px;
59 | /* gutter size */
60 | background-clip: padding-box;
61 | }
62 |
63 | /* Style your items */
64 | .my-masonry-grid_column>div {
65 | /* change div to reference your elements you put in */
66 | background: grey;
67 | margin-bottom: 10px;
68 | }
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ImageCard from "@/components/ui/image-card";
4 | import { useQuery, useQueryClient } from "@tanstack/react-query";
5 | import { useEffect, useState } from "react";
6 | import Masonry from "react-masonry-css";
7 | import InfiniteScroll from "react-infinite-scroll-component";
8 |
9 | export interface IWaifu {
10 | id: number;
11 | image_id: string;
12 | original_image: string;
13 | thumbnail: string;
14 | blur_data_url: string;
15 | is_nsfw: boolean;
16 | width: number;
17 | height: number;
18 | creator_name: string;
19 | creator_username: string;
20 | caption: string;
21 | source: string;
22 | created_at: string;
23 | updated_at: string;
24 | }
25 |
26 | interface IWaifuResponse {
27 | count: number;
28 | next: string | null;
29 | previous: string | null;
30 | results: IWaifu[];
31 | }
32 |
33 | export default function Home() {
34 | const [waifus, setWaifus] = useState([]);
35 | const [nextUrl, setNextUrl] = useState(
36 | "https://api.animemoe.us/waifu/"
37 | );
38 | const [hasMore, setHasMore] = useState(true);
39 |
40 | const queryClient = useQueryClient();
41 |
42 | // Initial data fetch
43 | const {
44 | isLoading: isLoadingWaifuResponse,
45 | isError: isErrorWaifuResponse,
46 | error: errorWaifuResponse,
47 | } = useQuery({
48 | queryKey: ["home"],
49 | queryFn: async () => {
50 | if (!nextUrl)
51 | return { count: 0, next: null, previous: null, results: [] };
52 |
53 | const response = await fetch(nextUrl);
54 | if (!response.ok) {
55 | throw new Error(`API error: ${response.status}`);
56 | }
57 |
58 | const data = await response.json();
59 |
60 | // Update state with fetched data
61 | setWaifus((prevWaifus) => [...prevWaifus, ...data.results]);
62 | setNextUrl(data.next);
63 | setHasMore(!!data.next);
64 |
65 | return data;
66 | },
67 | enabled: !!nextUrl,
68 | });
69 |
70 | // Function to fetch more data when scrolling
71 | const fetchMoreData = async () => {
72 | if (!nextUrl) {
73 | setHasMore(false);
74 | return;
75 | }
76 |
77 | try {
78 | const response = await fetch(nextUrl);
79 | if (!response.ok) {
80 | throw new Error(`API error: ${response.status}`);
81 | }
82 |
83 | const data: IWaifuResponse = await response.json();
84 |
85 | setWaifus((prevWaifus) => [...prevWaifus, ...data.results]);
86 | setNextUrl(data.next);
87 | setHasMore(!!data.next);
88 |
89 | // Update query cache
90 | queryClient.setQueryData(["home"], data);
91 | } catch (error) {
92 | console.error("Error fetching more data:", error);
93 | }
94 | };
95 |
96 | return (
97 |
98 | {isLoadingWaifuResponse && waifus.length === 0 && (
99 |
Loading...
100 | )}
101 | {isErrorWaifuResponse && (
102 |
Error loading data
103 | )}
104 |
105 |
106 | instagram.com/arter_tendean
107 |
108 |
109 |
Loading more...}
114 | endMessage={No more images to load
}
115 | >
116 |
121 | {waifus.map((waifu) => (
122 |
123 |
131 |
132 | ))}
133 |
134 |
135 |
136 | );
137 | }
138 |
--------------------------------------------------------------------------------