{"id":20610813,"url":"https://github.com/anjasfedo/animevaultnextjs","last_synced_at":"2025-10-29T21:32:12.778Z","repository":{"id":209684123,"uuid":"724701629","full_name":"Anjasfedo/animeVaultNextJS","owner":"Anjasfedo","description":"NextJS Server Actions, Infinite Scroll and Framer Motion.","archived":false,"fork":false,"pushed_at":"2023-12-19T16:01:49.000Z","size":1601,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-17T03:28:37.933Z","etag":null,"topics":["framer-motion","frontend","learning-by-doing","nextjs","react","server-actions"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Anjasfedo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-11-28T16:08:22.000Z","updated_at":"2024-01-11T12:19:07.000Z","dependencies_parsed_at":"2024-01-09T03:02:13.408Z","dependency_job_id":"f8fccb6d-79c5-4c0c-b2a3-ea0b752ba744","html_url":"https://github.com/Anjasfedo/animeVaultNextJS","commit_stats":null,"previous_names":["g1a021037-anjasfedo/animevaultnextjs","anjasfedo/animevaultnextjs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Anjasfedo%2FanimeVaultNextJS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Anjasfedo%2FanimeVaultNextJS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Anjasfedo%2FanimeVaultNextJS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Anjasfedo%2FanimeVaultNextJS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Anjasfedo","download_url":"https://codeload.github.com/Anjasfedo/animeVaultNextJS/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242258191,"owners_count":20098282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framer-motion","frontend","learning-by-doing","nextjs","react","server-actions"],"created_at":"2024-11-16T10:17:55.194Z","updated_at":"2025-10-29T21:32:12.704Z","avatar_url":"https://github.com/Anjasfedo.png","language":"TypeScript","readme":"# Build Modern Next 14 Server Side App with Server Actions, Infinite Scroll \u0026 Framer Motion Animations\n\n\n## 1. Server Actions Crash Course\n\nnextjs have server component, and after that they release server actions\n\nserver actions that can make is a function run on server, but we can use it like an ordinary function of javascript\n\nwe can use it by using: \"use server\"\n\nfor example a client react component that have an async function, on that function use \"use server\": \"use client\"\n\nasync function requestUserName(formData) {\n    \"use server\"\n    const username = formData.get(\"username\")\n    // ...\n}\n\nexport default function App() {\n    return (\n        \u003cform action={requestUserName}\u003e\n            \u003cinput type=\"text\" name=\"username\" /\u003e\n            \u003cbutton type=\"submit\u003eRequest\u003c/button\u003e\n        \u003c/form\u003e\n    )\n}\n\nthe code above is some how equal to this code: \"use client\"\n\nasync function requestUserName(formData) {\n    const response = await fetch(\"some_url\", {\n        method: \"POST\",\n        header: {\n            Content-Type: \"application/json\"\n            // Add another if needer\n        },\n        body: JSON.Stringfy({\n            username: fromData.get(\"username\"),\n            // Another request payload\n        })\n    })\n}\n\nexport default function App() {\n    return (\n        \u003cform action={requestUserName}\u003e\n            \u003cinput type=\"text\" name=\"username\" /\u003e\n            \u003cbutton type=\"submit\u003eRequest\u003c/button\u003e\n        \u003c/form\u003e\n    )\n}\n\nwe use server action to reduce our code, less the code. so we can focuse with business logic.\n\nwith api we not only need to make request, we also need to make the api\n\nbut with server action, we dont need api at all, because nextjs handle it\n\nthe problem of server action is because they use Post method, for now server action is not compitable for another device(not web)\n\nserver action less our client side code. server action still work event javascript is disable, because it use server side render\n\nabout the spinner, because it logic is on browser, they are pure client client component\n\nthe advantage of server action:\nless client code\npage load faster\nbetter response\ngood for seo\nalso improve core web vitals, crawl budget, crawl ranking\nand user exprience(ux)\nand most importanly they improve dx(developer exprience), because we can develop faster\n\n\nserver action not only can use just with client component, we can also use server action on server component like: import { fetchAnimes } from \"./action\"\n\nasync function Home(formData) {\n    const animes = await fetchAnimes(1)\n    return (\n        \u003cmain\u003e\n            \u003csection\u003e\n                {animes}\n            \u003c/section\u003e\n            \u003cLoadMore /\u003e\n        \u003c/main\u003e\n    )\n}\n\nexport default Home()\n\nfor the fetchAnimes like: \"use server\"\n\nexport async function fetchAnimes(page: number) :Promise\u003cAnimeCard[]\u003e {\n    const response = await fetch(\"some-url\")\n    \n    const data = await response.json()\n}\n\n\nwhy post method use on fetch anime,\nbecause we actualy use get method, where we get a whole html page. because the function is in server, so it will give html page to client\n\nbut we also post use on scroll, we request the component on api\n\nwe can use server action to get list of data. not only that it also can be use to mutation (crud)\n\n\n## 2. Implement Server Actions\n\n1. firstly we can clone the main branch of this github repositiry: https://github.com/adrianhajdin/anime_vault.git\nby use :git clone https://github.com/adrianhajdin/anime_vault.git\n\n2. open it on vscode, we get the initialize code, with data on /app/_data.ts that have 20 data of anime\n\n3. on /app/layout.tsx, we have Hero (header), Footer, some font, meta data, and the children\n\n4. install the dependecy with npm install\n\n5. to reduce the appearance of code, especialy of tailwind, we can use an extension name inline fold\n\n6. then we can npm run dev to run the program\n\n7. next we will use data fetch from api to get data of anime to show it\n\n8. now add new file on /app/action.ts, on that use server\n\n9. have an async function name fetchAnime, that fetch data from api with await on response variable. the api we use is https://shikimori.one/api/animes\n\n10. then make variable name data with value of await response.json(), and return the data like\n\n11. because this function is only declarative, we need to export it to use it on another react component with: \n\n12. open /app/page.tsx, and make a new variable name data that have value await fetchAnime, the function from action.ts like:\n\n13. next we can add query on the api by add ?page=1.\n\n14. we even can change the page value with argument of function, by change it with string literal that use the argument\n\n15. also we need to pass limit on the api, with and query where \u0026limit=8 \n\n16. and add another and query \u0026order=popularity on the api like:\n\n17. so we can back to /app/page.tsx, so we can pass a parameter for page on fetchAnime function\n\n18. and we successful get the data, but the page not show data properly\n\n19. define the type of index\n\n20. then on /component/AnimeCard.tsx.\n\n21. the image not show because we only get the end point of image url, so we can add domain to use it like: `http://shikimori.one${anime.image.original}`\n\n22. so we get a page and 8 of anime data show up on our app\n\n23. next we will use LoadMore to scroll and get new page, new anime data\n\n\n## 3. Infinite Scroll in Next 14\n1. open /components/LoadMore.tsx, we will load more card on this page, with paginate. so when we scroll it will fetch more data\n\n2. the main on here is instead of trigger fetch with button, we will fetch by scroll on spesific on screen(end of screen)\n\n3. to do that, we can use a library, install it with: npm install react-intersection-observer\n\n4. so now we can use it by: import { useInView } from \"react-intersection-observer\"\n\n5. to use that we can create new variable that destruct an object have ref end inView prop with value useInView() like:   const { ref, inView } = useInView()\n\n6. because we use a hook, add \"use client\" on top of out page\n\n7. so all of page is render by server. but only the LoadMore component is render by client\n\n8. add an attribute ref equal to {ref} on div tag like:     \u003c\u003e\n      \u003csection className=\"flex justify-center items-center w-full\"\u003e\n        \u003cdiv ref={ref}\u003e\n          \u003cImage\n            src=\"./spinner.svg\"\n            alt=\"spinner\"\n            width={56}\n            height={56}\n            className=\"object-contain\"\n          /\u003e\n        \u003c/div\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n\n9. next we can use the useEffect hook, with dependecy of array inView like:   useEffect(() =\u003e {\n\n  }, [inView]);\n\n10. inside the useEffect we will add the logic, for now lets show alert if we on the end of page\n\n11. dont forget to create a conditional first if we get the inView like:   useEffect(() =\u003e {\n    if (inView) {\n      // logic here\n    }\n  }, [inView]);\n\n12. we can change the logic with our fetchAnime function from action.ts like:    useEffect(() =\u003e {\n    if (inView) {\n      fetchAnime(2)\n    }\n  }, [inView]);\n\n13. also we need to pass the argument of page number by 2 \n\n14. then we can use .then to get res, and set the res we get on data with useState hook, the initial value will be empty array like:   const [data, setData] = useState([]);\n\n  useEffect(() =\u003e {\n    if (inView) {\n      fetchAnime(2)\n        .then((res) =\u003e {\n          \n        })\n    }\n  }, [inView]);\n\n15. on the setData, we not only set the new data we get from fetch to the data, but we need to stay the old data\n\n16. to do that we can use spread operator of old data, also we spread the rest to add all res on setData like:  setData([...data, ...res])\n\n17. so we need to define the type of array on data useState, we can use AnimeProp of array on /.AnimeCard.tsx like:     const [data, setData] = useState\u003cAnimeProp[]\u003e([]);\n\n18. also add the array dependency on useEffect with data like:   useEffect(() =\u003e {\n    if (inView) {\n      fetchAnime(2)\n        .then((res) =\u003e {\n          setData([...data, ...res])\n        })\n    }\n  }, [inView, data]);\n\n19. because we already fetch anime data when we once open the page, but in LoadMore we need to show anime data too\n\n20. to do that we can copy the section that map the anime data on /app/page.tsx like: return (\n    \u003c\u003e\n      \u003csection className=\"grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-10\"\u003e\n        {data.map((item: AnimeProp, index: number) =\u003e (\n          \u003cAnimeCard key={item.id} anime={item} index={index} /\u003e\n        ))}\n      \u003c/section\u003e\n      \u003csection className=\"flex justify-center items-center w-full\"\u003e\n        \u003cdiv ref={ref}\u003e\n          \u003cImage\n            src=\"./spinner.svg\"\n            alt=\"spinner\"\n            width={56}\n            height={56}\n            className=\"object-contain\"\n          /\u003e\n        \u003c/div\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n\n21. also dont forget to import AnimeCard component, so this section is subsequent page from page one\n\n22. when we open our website, we can scroll the page to end, so we will get more anime card. but the image/data we get will be repeat if we scroll more\n\n23. to fixed it we need to implement a new variable that is Page, we have to track the page currently\n\n24. so we can declare a let variable on the /component/LoadMore.tsx, add variable name page with value 2 like: let page = 2;\n\n25. so below of setData, we can increment the page value, and also we use the page variable as argument that we passing on fetchAnime function like:     useEffect(() =\u003e {\n    if (inView) {\n      fetchAnime(page).then((res) =\u003e {\n        setData([...data, ...res]);\n        page++;\n      });\n    }\n  }, [inView, data]);\n26. so when we try the website, we now get unique image for every page\n\n## 4. Framer Motion Next 14\n1. to add animation, we will use framer motion\n2. since every components is server render, we need to do\n3. install framer motion with: npm i framer-motion\n4. then we can navigate to components/animeCard.tsx, we will implement the animation in here\n5. in react we can easy apply it, but in nextjs we use server rendering, and it will kinda hard\n6. we start with import motion from framer-motion\n7. then we can change div to motion.div and add attribute of variants with value variants:\n8. then make variable variants with value object of hidden and visible with following value:\n9. then add attribute initial with value hidden\n10. and we add attribute animate with value visible\n11. next add attribute transition with value an object have prop delay, ease, and duration\n12. and add attribute of viewport that have value an object with prop amount in it:\n13. so we will get this code for the div attribute:variants={variants}\n    initial=\"hidden\"\n    animate=\"visible\"\n    transition={{ \n      delay: 1,\n      ease: \"easeInOut\",\n      duration: 0.5\n     }}\n     viewport={{ amount: 0 }}\n14. now if we see the app, we will get an error, because we use serverside rendering\n15. we can change this div to new component\n16. then we make new component on components name MotionDiv\n17. this component become client component and also import the motion from framer motion. then export motion.div like: \"use client\";\nimport { motion } from \"framer-motion\";\n\nexport const MotionDiv = motion.div;\n\n18. so now the client side render will only render the div, and rest of them will be server side render\n19. for now our animation its not good enough, because when we reload the page, we can notice that there is nothing and then all appear at once\n20. next we well add animation on AnimeCard of Page and LoadMore\n21. we can accept index as prop on AnimeCard component\n20. and we can stagger the animation for every subsequent card\n22. for delay we can use index * 0.25, delay will increment for all index:       delay: index * 0.25,\n21. And we get another problem, when we scroll all to button, we need time to wait load process\n22. to fix it we can do apply the stagger on spesific page.\n23. now we can open our action on /app/action.ts\n24. on it instead  only return data, we also can return the component themself\n25. let copy the entire data.map on /app/page.tsx\n26. then we can retun data as map, and then we need to change the file action.ts to action.tsx, we get action like: \"use server\";\n\nimport AnimeCard, { AnimeProp } from \"@/components/AnimeCard\";\n\nexport const fetchAnime = async (page: number) =\u003e {\n  const response = await fetch(\n    `https://shikimori.one/api/animes?page=${page}\u0026limit=8\u0026order=popularity`\n  );\n\n  const data = await response.json();\n\n  return data.map((item: AnimeProp, index: number) =\u003e (\n    \u003cAnimeCard key={item.id} anime={item} index={index} /\u003e\n  ))\n};\n\n27. and we need to import AnimeCard and AnimeProp from AnimeCard.tsx, like:\n28. because we already give data as map. we can simply delete map method on page.tsx and LoadMore.tsx like: \u003csection className=\"grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-10\"\u003e\n        {data}\n      \u003c/section\u003e\n29. now we get error from typescript. we can do export type of AnimeCard with value JSX.Element like: export type AnimeCard = JSX.Element;\n30. and now we can replace the AnimeProp with AnimeCard\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanjasfedo%2Fanimevaultnextjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanjasfedo%2Fanimevaultnextjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanjasfedo%2Fanimevaultnextjs/lists"}