V1 without presentation styling
@@ -1,16 +0,0 @@
|
||||
import { getRandomWordList } from "@/words/word-list/utils";
|
||||
|
||||
const getServerSideProps = async () => {
|
||||
const wordList = await getRandomWordList();
|
||||
console.log("Data is being fetch...", wordList)
|
||||
return { props: { wordList:wordList } }
|
||||
}
|
||||
|
||||
export default async function WordList(){
|
||||
const wordList = await getServerSideProps();
|
||||
return (
|
||||
<div data-testid="word-list-view">
|
||||
WordList server props: {JSON.stringify(wordList)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './word-list.view'
|
||||
@@ -1,15 +0,0 @@
|
||||
import { WordListRepsonse } from './word-list.types'
|
||||
|
||||
const dataURL = 'http://localhost:3000/words/es?complexity=easy&howMany=10'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const randomWordList = await getRandomWordList();
|
||||
return {
|
||||
props: { randomWordList }, // will be passed to the page component as props
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomWordList() {
|
||||
const wordList: WordListRepsonse = await fetch(dataURL).then((response) => response.json());
|
||||
return wordList;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { WordListView } from './word-list.view'
|
||||
|
||||
const meta: Meta<typeof WordListView> = {
|
||||
title: 'WordListView',
|
||||
component: WordListView,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof WordListView>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('word-list-view')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export type WordElement = {
|
||||
word: string
|
||||
correct?: boolean
|
||||
}
|
||||
|
||||
export type WordListRepsonse = {
|
||||
wordList: WordElement[]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import styles from './word-list.module.css'
|
||||
import { InferGetServerSidePropsType, InferGetStaticPropsType } from 'next'
|
||||
import { getRandomWordList, getStaticProps } from './utils'
|
||||
|
||||
export function WordListView(
|
||||
props: InferGetStaticPropsType<typeof getStaticProps>
|
||||
) {
|
||||
return (
|
||||
<div data-testid="word-list-view" className={styles.container}>
|
||||
WordList: {JSON.stringify(props?.randomWordList)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
@@ -1,15 +0,0 @@
|
||||
# Generated files
|
||||
node_modules
|
||||
output
|
||||
dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "presentation-animations",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@motion-canvas/core": "^3.11.0",
|
||||
"@motion-canvas/2d": "^3.11.0",
|
||||
"@motion-canvas/ffmpeg": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@motion-canvas/ui": "^3.11.0",
|
||||
"@motion-canvas/vite-plugin": "^3.11.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.4"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="@motion-canvas/core/project" />
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"version": 0,
|
||||
"shared": {
|
||||
"background": null,
|
||||
"range": [
|
||||
0,
|
||||
null
|
||||
],
|
||||
"size": {
|
||||
"x": 1920,
|
||||
"y": 1080
|
||||
},
|
||||
"audioOffset": 0
|
||||
},
|
||||
"preview": {
|
||||
"fps": 30,
|
||||
"resolutionScale": 1
|
||||
},
|
||||
"rendering": {
|
||||
"fps": 60,
|
||||
"resolutionScale": 1,
|
||||
"colorSpace": "srgb",
|
||||
"exporter": {
|
||||
"name": "@motion-canvas/core/image-sequence",
|
||||
"options": {
|
||||
"fileType": "image/png",
|
||||
"quality": 100,
|
||||
"groupByScene": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import example from './scenes/example?scene';
|
||||
|
||||
export default makeProject({
|
||||
scenes: [example],
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": 0,
|
||||
"timeEvents": [],
|
||||
"seed": 1375832605
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import {makeScene2D, Circle} from '@motion-canvas/2d';
|
||||
import {all, createRef} from '@motion-canvas/core';
|
||||
|
||||
export default makeScene2D(function* (view) {
|
||||
const myCircle = createRef<Circle>();
|
||||
|
||||
view.add(
|
||||
<Circle
|
||||
ref={myCircle}
|
||||
// try changing these properties:
|
||||
x={-300}
|
||||
width={140}
|
||||
height={140}
|
||||
fill="#e13238"
|
||||
/>,
|
||||
);
|
||||
|
||||
yield* all(
|
||||
myCircle().position.x(300, 1).to(-300, 1),
|
||||
myCircle().fill('#e6a700', 1).to('#e13238', 1),
|
||||
);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@motion-canvas/2d/tsconfig.project.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import motionCanvas from '@motion-canvas/vite-plugin';
|
||||
import ffmpeg from '@motion-canvas/ffmpeg';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
motionCanvas(),
|
||||
ffmpeg(),
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
},
|
||||
// Avoiding CORS issues
|
||||
// async rewrites() {
|
||||
// return [
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
presentation/public/server-side-rendering-diagram.avif
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
140
presentation/src/app/presentation/page.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Presentation() {
|
||||
const [current, setCurrent] = useState<number>(0)
|
||||
|
||||
const stages = [
|
||||
<Introduction />,
|
||||
<Benefits />,
|
||||
<Rendering />,
|
||||
<Strategies />,
|
||||
<DataFetching />,
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="main-presentation">
|
||||
{stages[current]}
|
||||
<button
|
||||
onClick={() =>
|
||||
setCurrent(current + 1 < stages.length ? current + 1 : 0)
|
||||
}
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Introduction
|
||||
// 2. Rendering strategies.
|
||||
// 3. Benefits.
|
||||
// 4. Actual process and why its important to know it.
|
||||
// 5. Serverside data fetching
|
||||
|
||||
function Introduction() {
|
||||
return (
|
||||
<div className="main-presentation-comparation">
|
||||
<h3>Next</h3>
|
||||
<div>
|
||||
<p>Nos da la posibilidad de renderizar desde el servidor</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Rendering() {
|
||||
return (
|
||||
<div className="main-presentation-comparation">
|
||||
<h3>Rendering</h3>
|
||||
<div>
|
||||
The rendering works is split into chunks: - By individual route
|
||||
segments - By React [Suspense
|
||||
boundaries](https://react.dev/reference/react/Suspense) (react
|
||||
way of having a fallback while a components has finished
|
||||
loading) Each chunk is rendered in the server, then, on the
|
||||
client: 1. The HTML is used to immediately show fast preview. 2.
|
||||
The server components rendered are inserted to update the DOM
|
||||
(components rendered in server with placeholders for client
|
||||
components and props). 3. `JS` instructions are used to
|
||||
[hydrate?](https://react.dev/reference/react-dom/client/hydrateRoot)
|
||||
Client Components and make the application interactive.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Strategies() {
|
||||
return (
|
||||
<div className="main-presentation-strategies">
|
||||
<h3>Strategies</h3>
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
- Static rendering (default): Good for static pages:
|
||||
Rendered in build time or in the background after data
|
||||
revalidation
|
||||
</li>
|
||||
<li>
|
||||
- Security: sensitive data is kept int the server (API
|
||||
keys and tokens)
|
||||
</li>
|
||||
<li>
|
||||
- Dynamic rendering: rendered per user request, Next
|
||||
uses this type of rendering automatically when discovers
|
||||
a dynamic function (`cookies()`, `headers()`,
|
||||
`userSearchParams()`).
|
||||
</li>
|
||||
<li>
|
||||
- Streaming: work is split into chunks and streamed as
|
||||
they become ready so the load is progressive.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Benefits() {
|
||||
return (
|
||||
<div className="main-presentation-benefits">
|
||||
<h3>Beneficios</h3>
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
- Fetch data directly on the server, performance and
|
||||
load benefits.
|
||||
</li>
|
||||
<li>
|
||||
- Security: sensitive data is kept int the server (API
|
||||
keys and tokens)
|
||||
</li>
|
||||
<li>
|
||||
- Caching: results can be cached to improve performance
|
||||
between users.
|
||||
</li>
|
||||
<li>
|
||||
- Bundle size: will be reduced as part of the
|
||||
application will reside in the server.
|
||||
</li>
|
||||
<li>
|
||||
- SEO: because the pages will be rendered the search
|
||||
engine bots will make good use of it.
|
||||
</li>
|
||||
<li>
|
||||
- Streaming: to split the rendering into chunks and
|
||||
stream them as they become ready.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DataFetching() {
|
||||
const router = useRouter()
|
||||
router.push('/word-list')
|
||||
return <>Redirecting...</>
|
||||
}
|
||||
6
presentation/src/app/word-list/actions.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
'use server'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function revalidateFetchByTag(tag: string) {
|
||||
revalidateTag(tag)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { WordListRepsonse, fetchWordlist } from '../utils'
|
||||
|
||||
export default function ClientWordList() {
|
||||
const [data, setData] = useState<WordListRepsonse>()
|
||||
|
||||
const fetchData = () =>
|
||||
fetchWordlist().then((response) => setData(response))
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="data-fetching-client">
|
||||
<h3>
|
||||
Wordlist fetched in the <span>client</span>
|
||||
</h3>
|
||||
<div>
|
||||
{data?.wordList.map((word) => (
|
||||
<span>{word.word}</span>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={fetchData}>Revalidate!</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
presentation/src/app/word-list/components/go-back-button.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function GoBackButton() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<button
|
||||
style={{ position: 'absolute' }}
|
||||
onClick={() => router.push('/presentation')}
|
||||
>
|
||||
Go back to presentation
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { revalidateFetchByTag } from '../actions'
|
||||
|
||||
export default function RevalidateButton() {
|
||||
return (
|
||||
<button onClick={() => revalidateFetchByTag('wordlist')}>
|
||||
Revalidate!
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import RevalidateButton from './revalidate-button'
|
||||
import { fetchWordlist } from '../utils'
|
||||
|
||||
export default async function ServerWordList() {
|
||||
const response = await fetchWordlist()
|
||||
|
||||
return (
|
||||
<div data-testid="word-list-view" className="data-fetching-server">
|
||||
<h3>
|
||||
Wordlist fetched in the <span>server</span>
|
||||
</h3>
|
||||
<div>
|
||||
{response?.wordList.map((word) => (
|
||||
<span>{word.word}</span>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<RevalidateButton />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
presentation/src/app/word-list/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import ClientWordList from './components/clientside-word-list'
|
||||
import GoBackButton from './components/go-back-button'
|
||||
import ServerWordList from './components/serverside-word-list'
|
||||
|
||||
export default function WordList() {
|
||||
return (
|
||||
<div className="data-fetching">
|
||||
<ServerWordList />
|
||||
<ClientWordList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
presentation/src/app/word-list/utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const WORDLIST_API_URL =
|
||||
'http://localhost:3000/words/es?complexity=medium&howMany=30'
|
||||
|
||||
export type WordElement = {
|
||||
word: string
|
||||
correct?: boolean
|
||||
}
|
||||
|
||||
export type WordListRepsonse = {
|
||||
wordList: WordElement[]
|
||||
}
|
||||
|
||||
// fetch("https://...", { cache: "no-store" });
|
||||
export const fetchWordlist = async () => {
|
||||
const wordList: WordListRepsonse = await fetch(WORDLIST_API_URL, {
|
||||
next: { tags: ['wordlist'] },
|
||||
}).then((response) => response.json())
|
||||
|
||||
console.log('Data fetch is done', wordList)
|
||||
|
||||
return wordList
|
||||
}
|
||||