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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
serverActions: true,
|
||||||
|
},
|
||||||
// Avoiding CORS issues
|
// Avoiding CORS issues
|
||||||
// async rewrites() {
|
// async rewrites() {
|
||||||
// return [
|
// 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
|
||||||
|
}
|
||||||