├── .gitignore ├── README.md ├── examples ├── animated-counter │ ├── index.html │ ├── script.js │ └── style.css ├── basic-intro │ ├── index.html │ └── reversed.html ├── basic-sorting │ ├── index.html │ ├── population-reversed.html │ ├── population.html │ ├── reversed.html │ └── style.css ├── filtering-client │ ├── index.html │ └── script.js ├── filtering-react │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.jsx │ │ └── main.jsx │ └── vite.config.js ├── filtering-ssg │ ├── build.mjs │ └── style.css └── filtering-ssr │ ├── server.mjs │ └── style.css ├── package.json └── src ├── components ├── details.astro ├── genres.astro ├── shell.astro └── shows.astro ├── data ├── content.json └── genres.json ├── env.d.ts ├── modules └── local.ts └── pages ├── browse ├── [id].astro ├── genre-[key].astro └── index.astro ├── favourites.astro └── index.astro /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | node_modules 3 | .astro 4 | package-lock.json 5 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ✨ CSS View Transitions Resources 3 | 4 | ⭐ **If you find this content useful please give it a star on Github.** ⭐ 5 | 6 | - [Overview](#overview) 7 | - [Full Example](#full-example) 8 | - [Basic Examples](#basic-examples) 9 | - [Without JavaScript](#without-javascript) 10 | - ["Hello World" (examples/basic-intro)](#hello-world-examplesbasic-intro) 11 | - [Basic Sorting (examples/basic-sorting)](#basic-sorting-examplesbasic-sorting) 12 | - [Static Site Generation (SSG) (examples/filtering-ssg)](#static-site-generation-ssg-examplesfiltering-ssg) 13 | - [Server Side Rendering (SSR) (examples/filtering-ssr)](#server-side-rendering-ssr-examplesfiltering-ssr) 14 | - [With JavaScript](#with-javascript) 15 | - [Vanilla JavaScript (examples/filtering-client)](#vanilla-javascript-examplesfiltering-client) 16 | - [Custom Animations (examples/custom-animations)](#custom-animations-examplescustom-animations) 17 | - [Using with React (examples/filtering-react)](#using-with-react-examplesfiltering-react) 18 | - [Full Site (Astro SSG) (examples/full-site)](#full-site-astro-ssg-examplesfull-site) 19 | - [Other Examples](#other-examples) 20 | - [Reading Material](#reading-material) 21 | 22 | 23 | # Overview 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 43 | 44 |

YouTube Video

Slides Link

33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 |
45 | 46 | Note that if you are interested in similar functionality in the world of scroll-driven animations then also check out the following video Justin and myself did with a specific focus on scroll-driven animation: [https://www.youtube.com/watch?v=XpGsLlx00fU](https://www.youtube.com/watch?v=XpGsLlx00fU) 47 | 48 | # Full Example 49 | 50 | 51 | 52 | 53 | 54 | 55 | The following is an example demo site that uses CSS View Transitions between HTML page navigations. Note that there is no JavaScript* used here, it is exclusively just regular `` navigations to HTML pages. You can check the URL change as you use the example. 56 | 57 | \* JavaScript is used only to save/get your selected favourites in the browser local storage, and not used in any navigations and/or animation. 58 | 59 | The only requirements are adding the following CSS: 60 | 61 | ```css 62 | @view-transition { 63 | navigation: auto; 64 | } 65 | ``` 66 | 67 | After adding the above CSS you then need to assign a unique `view-transition-name:` via CSS (I just added it as an inline style, i.e. `
  • ` on the both the starting and ending pages), and then browser should automatically animate the same element between the two pages as your navigate. 68 | 69 | All code is generated as plain HTML via Astro SSG from the `src` folder. It is automatically deployed from this repo to `https://css-view-transition.vercel.app/`. All other (smaller scale) examples are in the `examples` folder. 70 | 71 | ## Basic Examples 72 | 73 | Note that for these smaller examples, it is assumed that you merely care about the code used to create the examples, and that actually testing the examples out are of secondary concern. For this reason, they aren't deployed online (unlike the full demo). However, you are welcome to clone/download this repository and then follow the steps below, if you want to see them in action. 74 | 75 | ## Without JavaScript 76 | 77 | ### "Hello World" ([examples/basic-intro](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/basic-intro)) 78 | 79 | 80 | 81 | 82 | 83 | You should be able to open either `index.html` or `reversed.html` directly in your browser as a starting point. 84 | 85 | ### Basic Sorting ([examples/basic-sorting](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/basic-sorting)) 86 | 87 | 88 | 89 | 90 | 91 | This example uses an external stylesheet, so it is recommended that you run it through HTTP (and not open the file directly). If you are using VS Code, then you can use [the following extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer). Alternatively, you should be able to use any server emulation software as well (for example XAMPP, etc.) 92 | 93 | ### Static Site Generation (SSG) ([examples/filtering-ssg](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/filtering-ssg)) 94 | 95 | 96 | 97 | 98 | 99 | In addition to using HTTP (as per the above example). The actual HTML files itself is generated by means of running the `build.mjs` file via node: `node build.mjs`. If you are doing Static Site Generation you are probably using [Astro](https://astro.build/), [11ty](https://www.11ty.dev/) or [one of the following tools](https://jamstack.org/generators/) - the `build.mjs` is just to show the concept with as minimal code as possible. 100 | 101 | If you want to test the example above you need [Node](https://nodejs.org/en) installed locally, however the principles/concept should be exactly the same regardless of what tool you are using for SSG. 102 | 103 | ### Server Side Rendering (SSR) ([examples/filtering-ssr](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/filtering-ssr)) 104 | 105 | 106 | 107 | 108 | 109 | Similar to the above you need HTTP and Node if you want to run the actual example. 110 | 111 | However, if you are doing purely server-side rendering you are probably using Express / NestJS / Fastify (Node), Django / Flask (Python), or Laravel / Wordpress (PHP). However, the `server.mjs` file in the example is just to show the concept with as minimal code as possible. You can start the server by running `node server.mjs`. 112 | 113 | ## With JavaScript 114 | 115 | ### Vanilla JavaScript ([examples/filtering-client](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/filtering-client)) 116 | 117 | 118 | 119 | 120 | 121 | This example is a single HTML page that simply pulls in a `script.js` file to generate the content in the browser itself with Javascript. This example is effectively the same as all examples above, but uses the JavaScript `document.startViewTransition` method instead of the `@view-transition` CSS rule. 122 | 123 | You should be able to simply open the `index.html` directly, but it is recommended that you use HTTP to serve the file locally. 124 | 125 | In the example the HTML is simply replaced with each change via the `innerHTML` method (instead of moving actual DOM nodes around). This is significant since it means that the animation uses the `view-transition-name` assignment, to determime when something is the same element (for animation purposes) regardless whether it is a completely newly created HTML DOM node. Note `view-transition-name` only influences the animation, and other rules around DOM references (for example focus state) still apply. 126 | 127 | ### Custom Animations ([examples/custom-animations](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/animated-counter)) 128 | 129 | 130 | 131 | 132 | 133 | In this example, there aren't any actual changes to the HTML/DOM structure. The `innerText` value of the `0` is simply updated based on whether the plus/minus buttons are clicked. 134 | 135 | ### Using with React ([examples/filtering-react](https://github.com/schalkventer/css-view-transition-resources/tree/main/examples/filtering-react)) 136 | 137 | 138 | 139 | 140 | 141 | Note that when using React, due to React automatically resolving/evaluating state updates in parallel/asynchronously under the hood you need to wrap your `document.startViewTransition` method with the `flushSync` function provided by `react-dom` (as in the example). 142 | 143 | ## Other Examples 144 | 145 | - [Astro Records](https://astro-records.pages.dev/) 146 | - [Astro Movies](https://github.com/charca/astro-movies) 147 | 148 | ## Reading Material 149 | 150 | - [Pattern: View Transitions](https://www.patterns.dev/vanilla/view-transitions/) 151 | - [Chrome for Developers: Smooth transitions with the View Transition API](https://developer.chrome.com/docs/web-platform/view-transitions) 152 | - [Dave Rupert: Getting started with View Transitions on multi-page apps](https://daverupert.com/2023/05/getting-started-view-transitions/) 153 | -------------------------------------------------------------------------------- /examples/animated-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodePen - CSS View Transition Example 6 | 7 | 8 | 9 | 10 | 11 | 0 12 | 13 | 14 |
    15 | Your browser does not support CSS View Transitions - the example will 16 | still work, just without the animations. 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/animated-counter/script.js: -------------------------------------------------------------------------------- 1 | const subtract = document.querySelector("#subtract"); 2 | const value = document.querySelector("#value"); 3 | const add = document.querySelector("#add"); 4 | 5 | const animate = (callback) => { 6 | if (!document.startViewTransition) return callback(); 7 | document.startViewTransition(callback); 8 | }; 9 | 10 | const update = (amount) => { 11 | const isReversed = value.classList.contains("reverse"); 12 | if (!isReversed && amount < 0) value.classList.add("reverse"); 13 | if (isReversed && amount >= 0) value.classList.remove("reverse"); 14 | const current = parseInt(value.innerText); 15 | 16 | animate(() => { 17 | value.innerText = (current + amount).toString(); 18 | }); 19 | }; 20 | 21 | subtract.addEventListener("click", () => update(-1)); 22 | add.addEventListener("click", () => update(1)); -------------------------------------------------------------------------------- /examples/animated-counter/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | @keyframes slide-in { 6 | from { 7 | opacity: 0; 8 | transform: translateY(-11rem) scale(0.8); 9 | } 10 | 11 | to { 12 | opacity: 1; 13 | transform: translateY(0) scale(1); 14 | } 15 | } 16 | 17 | @keyframes slide-out { 18 | from { 19 | opacity: 1; 20 | transform: translateY(0) scale(1); 21 | } 22 | 23 | to { 24 | opacity: 0; 25 | transform: translateY(11rem) scale(0.8); 26 | } 27 | } 28 | 29 | html::view-transition-old(forward) { 30 | animation-duration: 0.4s; 31 | animation-name: slide-out; 32 | } 33 | 34 | html::view-transition-new(forward) { 35 | animation-duration: 0.4s; 36 | animation-name: slide-in; 37 | } 38 | 39 | html::view-transition-old(back) { 40 | animation-duration: 0.2s; 41 | animation-name: slide-in; 42 | animation-direction: reverse; 43 | } 44 | 45 | html::view-transition-new(back) { 46 | animation-duration: 0.2s; 47 | animation-name: slide-out; 48 | animation-direction: reverse; 49 | } 50 | 51 | @media (prefers-reduced-motion) { 52 | html::view-transition { 53 | display: none; 54 | } 55 | } 56 | 57 | body { 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | font-family: sans-serif; 62 | width: 100%; 63 | height: 100vh; 64 | overflow: hidden; 65 | gap: 3rem; 66 | } 67 | 68 | div { 69 | text-align: center; 70 | position: fixed; 71 | padding: 1rem 2rem; 72 | background: black; 73 | bottom: 0; 74 | left: 0; 75 | width: 100%; 76 | font-family: sans-serif; 77 | color: white; 78 | display: none; 79 | } 80 | 81 | @supports not (view-transition-name: example) { 82 | div { 83 | display: block; 84 | } 85 | } 86 | 87 | button { 88 | border: 1px solid blue; 89 | height: 5rem; 90 | width: 5rem; 91 | background: rgba(0, 0, 255, 1); 92 | border-radius: 8px; 93 | color: white; 94 | font-size: 4rem; 95 | cursor: pointer; 96 | view-transition-clase: button; 97 | user-select: none; 98 | border: 3px solid blue; 99 | line-height: 1; 100 | } 101 | 102 | button:focus-visible { 103 | border: 3px solid white; 104 | outline: 6px solid black; 105 | } 106 | 107 | button:hover { 108 | background: rgba(0, 0, 255, 0.8); 109 | } 110 | 111 | button:active { 112 | background: rgba(0, 0, 255, 0.7); 113 | } 114 | 115 | span { 116 | font-size: 6em; 117 | font-weight: bold; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | width: 15rem; 122 | text-align: center; 123 | height: 10rem; 124 | padding: 0 1rem; 125 | border-radius: 32px; 126 | color: blue; 127 | background: rgba(0, 0, 255, 0.05); 128 | view-transition-name: forward; 129 | } 130 | 131 | span.reverse { 132 | view-transition-name: back; 133 | } 134 | -------------------------------------------------------------------------------- /examples/basic-intro/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | Swap 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/basic-intro/reversed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | Swap 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/basic-sorting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 35 | 36 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /examples/basic-sorting/population-reversed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 35 | 36 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /examples/basic-sorting/population.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 35 | 36 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /examples/basic-sorting/reversed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 35 | 36 | 218 | 219 | 220 | ``` 221 | -------------------------------------------------------------------------------- /examples/basic-sorting/style.css: -------------------------------------------------------------------------------- 1 | @view-transition { 2 | navigation: auto; 3 | } 4 | -------------------------------------------------------------------------------- /examples/filtering-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 |
    14 | 18 | 19 | Continents 20 | 21 | 29 | 30 | Sorting 31 | 32 | 45 |
    46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/filtering-client/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {'asia' | 'north-america' | 'europe' | 'africa' | 'south-america'} Continent 3 | */ 4 | 5 | /** 6 | * @typedef {'alphabetical' | 'alphabetical-reversed' | 'population' | 'population-reversed'} Sorting 7 | */ 8 | 9 | /** 10 | * @typedef {object} Country 11 | * @prop {string} id 12 | * @prop {string} flag 13 | * @prop {string} name 14 | * @prop {number} population 15 | * @prop {Continent} continent 16 | */ 17 | 18 | /** 19 | * @typedef {object} Options 20 | * @prop {Sorting} sorting 21 | * @prop {'all' | Continent} continent 22 | */ 23 | 24 | /** 25 | * @typedef {object} Options 26 | * @prop {Sorting} sorting 27 | * @prop {string} search 28 | * @prop {'all' | Continent} continent 29 | */ 30 | 31 | /** 32 | * @type {Country[]} 33 | */ 34 | const data = [ 35 | { 36 | id: "bd", 37 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/bd.svg", 38 | name: "Bangladesh", 39 | population: 168000000, 40 | continent: "asia", 41 | }, 42 | { 43 | id: "br", 44 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/br.svg", 45 | name: "Brazil", 46 | population: 214000000, 47 | continent: "south-america", 48 | }, 49 | { 50 | id: "cn", 51 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cn.svg", 52 | name: "China", 53 | population: 1410000000, 54 | continent: "asia", 55 | }, 56 | { 57 | id: "cd", 58 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cd.svg", 59 | name: "Democratic Republic of the Congo", 60 | population: 96000000, 61 | continent: "africa", 62 | }, 63 | { 64 | id: "eg", 65 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/eg.svg", 66 | name: "Egypt", 67 | population: 109000000, 68 | continent: "africa", 69 | }, 70 | { 71 | id: "et", 72 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/et.svg", 73 | name: "Ethiopia", 74 | population: 117000000, 75 | continent: "africa", 76 | }, 77 | { 78 | id: "de", 79 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/de.svg", 80 | name: "Germany", 81 | population: 84000000, 82 | continent: "europe", 83 | }, 84 | { 85 | id: "in", 86 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/in.svg", 87 | name: "India", 88 | population: 1370000000, 89 | continent: "asia", 90 | }, 91 | { 92 | id: "id", 93 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/id.svg", 94 | name: "Indonesia", 95 | population: 276000000, 96 | continent: "asia", 97 | }, 98 | { 99 | id: "ir", 100 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ir.svg", 101 | name: "Iran", 102 | population: 85000000, 103 | continent: "asia", 104 | }, 105 | { 106 | id: "jp", 107 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/jp.svg", 108 | name: "Japan", 109 | population: 125000000, 110 | continent: "asia", 111 | }, 112 | { 113 | id: "mx", 114 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/mx.svg", 115 | name: "Mexico", 116 | population: 126000000, 117 | continent: "north-america", 118 | }, 119 | { 120 | id: "ng", 121 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ng.svg", 122 | name: "Nigeria", 123 | population: 211000000, 124 | continent: "africa", 125 | }, 126 | { 127 | id: "pk", 128 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/pk.svg", 129 | name: "Pakistan", 130 | population: 229000000, 131 | continent: "asia", 132 | }, 133 | { 134 | id: "ph", 135 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ph.svg", 136 | name: "Philippines", 137 | population: 113000000, 138 | continent: "asia", 139 | }, 140 | { 141 | id: "ru", 142 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ru.svg", 143 | name: "Russia", 144 | population: 146000000, 145 | continent: "europe", 146 | }, 147 | { 148 | id: "th", 149 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/th.svg", 150 | name: "Thailand", 151 | population: 70000000, 152 | continent: "asia", 153 | }, 154 | { 155 | id: "tr", 156 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/tr.svg", 157 | name: "Turkey", 158 | population: 86000000, 159 | continent: "europe", 160 | }, 161 | { 162 | id: "us", 163 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/us.svg", 164 | name: "United States", 165 | population: 332000000, 166 | continent: "north-america", 167 | }, 168 | { 169 | id: "vn", 170 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/vn.svg", 171 | name: "Vietnam", 172 | population: 99000000, 173 | continent: "asia", 174 | }, 175 | ]; 176 | 177 | const elements = { 178 | list: document.querySelector("#list"), 179 | search: document.querySelector("#search"), 180 | continent: document.querySelector("#continent"), 181 | sorting: document.querySelector("#sorting"), 182 | }; 183 | 184 | const animate = (callback) => { 185 | if (!document.startViewTransition) return callback(); 186 | document.startViewTransition(callback); 187 | }; 188 | 189 | const getOptions = () => { 190 | return { 191 | sorting: elements.sorting.value, 192 | search: elements.search.value, 193 | continent: elements.continent.value, 194 | }; 195 | }; 196 | 197 | const render = () => { 198 | const { sorting, continent, search } = getOptions(); 199 | 200 | const filtered = data.filter((country) => { 201 | if ( 202 | search !== "" && 203 | !country.name 204 | .toLowerCase() 205 | .includes(search.toLowerCase()) 206 | ) { 207 | return false; 208 | } 209 | 210 | if ( 211 | continent !== "all" && 212 | country.continent !== continent 213 | ) { 214 | return false; 215 | } 216 | 217 | return true; 218 | }); 219 | 220 | const sorted = filtered.sort((a, b) => { 221 | if (sorting === "alphabetical") { 222 | return a.name.localeCompare(b.name); 223 | } 224 | 225 | if (sorting === "alphabetical-reversed") { 226 | return b.name.localeCompare(a.name); 227 | } 228 | 229 | if (sorting === "population") { 230 | return a.population - b.population; 231 | } 232 | 233 | if (sorting === "population-reversed") { 234 | return b.population - a.population; 235 | } 236 | 237 | return 0; 238 | }); 239 | 240 | animate(() => { 241 | list.innerHTML = sorted 242 | .map(({ id, flag, name }) => { 243 | return ` 244 |
  • 245 | 250 | ${name} 251 |
  • 252 | `; 253 | }) 254 | .join(""); 255 | }); 256 | }; 257 | 258 | render(); 259 | 260 | elements.sorting.addEventListener("change", render); 261 | elements.search.addEventListener("input", render); 262 | elements.continent.addEventListener("change", render); 263 | -------------------------------------------------------------------------------- /examples/filtering-react/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /examples/filtering-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/filtering-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/filtering-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filtering-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.13.0", 14 | "@emotion/styled": "^11.13.0", 15 | "@mui/material": "^5.16.5", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.3.3", 21 | "@types/react-dom": "^18.3.0", 22 | "@vitejs/plugin-react": "^4.3.1", 23 | "eslint": "^8.57.0", 24 | "eslint-plugin-react": "^7.34.3", 25 | "eslint-plugin-react-hooks": "^4.6.2", 26 | "eslint-plugin-react-refresh": "^0.4.7", 27 | "vite": "^5.3.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/filtering-react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { flushSync } from "react-dom"; 3 | import styled from "@emotion/styled"; 4 | import { Global, css } from "@emotion/react"; 5 | 6 | import { 7 | TextField, 8 | FormControl, 9 | InputLabel, 10 | Select, 11 | MenuItem, 12 | CssBaseline, 13 | Paper, 14 | } from "@mui/material"; 15 | 16 | /** 17 | * @typedef {'asia' | 'north-america' | 'europe' | 'africa' | 'south-america'} Continent 18 | */ 19 | 20 | /** 21 | * @typedef {'alphabetical' | 'alphabetical-reversed' | 'population' | 'population-reversed'} Sorting 22 | */ 23 | 24 | /** 25 | * @typedef {object} Country 26 | * @prop {string} id 27 | * @prop {string} flag 28 | * @prop {string} name 29 | * @prop {number} population 30 | * @prop {Continent} continent 31 | */ 32 | 33 | /** 34 | * @typedef {object} Options 35 | * @prop {Sorting} sorting 36 | * @prop {'all' | Continent} continent 37 | */ 38 | 39 | /** 40 | * @typedef {object} Options 41 | * @prop {Sorting} sorting 42 | * @prop {string} search 43 | * @prop {'all' | Continent} continent 44 | */ 45 | 46 | /** 47 | * @type {Country[]} 48 | */ 49 | const data = [ 50 | { 51 | id: "bd", 52 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/bd.svg", 53 | name: "Bangladesh", 54 | population: 168000000, 55 | continent: "asia", 56 | }, 57 | { 58 | id: "br", 59 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/br.svg", 60 | name: "Brazil", 61 | population: 214000000, 62 | continent: "south-america", 63 | }, 64 | { 65 | id: "cn", 66 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cn.svg", 67 | name: "China", 68 | population: 1410000000, 69 | continent: "asia", 70 | }, 71 | { 72 | id: "cd", 73 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cd.svg", 74 | name: "Democratic Republic of the Congo", 75 | population: 96000000, 76 | continent: "africa", 77 | }, 78 | { 79 | id: "eg", 80 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/eg.svg", 81 | name: "Egypt", 82 | population: 109000000, 83 | continent: "africa", 84 | }, 85 | { 86 | id: "et", 87 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/et.svg", 88 | name: "Ethiopia", 89 | population: 117000000, 90 | continent: "africa", 91 | }, 92 | { 93 | id: "de", 94 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/de.svg", 95 | name: "Germany", 96 | population: 84000000, 97 | continent: "europe", 98 | }, 99 | { 100 | id: "in", 101 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/in.svg", 102 | name: "India", 103 | population: 1370000000, 104 | continent: "asia", 105 | }, 106 | { 107 | id: "id", 108 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/id.svg", 109 | name: "Indonesia", 110 | population: 276000000, 111 | continent: "asia", 112 | }, 113 | { 114 | id: "ir", 115 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ir.svg", 116 | name: "Iran", 117 | population: 85000000, 118 | continent: "asia", 119 | }, 120 | { 121 | id: "jp", 122 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/jp.svg", 123 | name: "Japan", 124 | population: 125000000, 125 | continent: "asia", 126 | }, 127 | { 128 | id: "mx", 129 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/mx.svg", 130 | name: "Mexico", 131 | population: 126000000, 132 | continent: "north-america", 133 | }, 134 | { 135 | id: "ng", 136 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ng.svg", 137 | name: "Nigeria", 138 | population: 211000000, 139 | continent: "africa", 140 | }, 141 | { 142 | id: "pk", 143 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/pk.svg", 144 | name: "Pakistan", 145 | population: 229000000, 146 | continent: "asia", 147 | }, 148 | { 149 | id: "ph", 150 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ph.svg", 151 | name: "Philippines", 152 | population: 113000000, 153 | continent: "asia", 154 | }, 155 | { 156 | id: "ru", 157 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ru.svg", 158 | name: "Russia", 159 | population: 146000000, 160 | continent: "europe", 161 | }, 162 | { 163 | id: "th", 164 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/th.svg", 165 | name: "Thailand", 166 | population: 70000000, 167 | continent: "asia", 168 | }, 169 | { 170 | id: "tr", 171 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/tr.svg", 172 | name: "Turkey", 173 | population: 86000000, 174 | continent: "europe", 175 | }, 176 | { 177 | id: "us", 178 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/us.svg", 179 | name: "United States", 180 | population: 332000000, 181 | continent: "north-america", 182 | }, 183 | { 184 | id: "vn", 185 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/vn.svg", 186 | name: "Vietnam", 187 | population: 99000000, 188 | continent: "asia", 189 | }, 190 | ]; 191 | 192 | /** 193 | * @type {object} 194 | * @prop {Sorting[]} sorting 195 | * @prop {('all' | Continent)[]} continents 196 | */ 197 | const arrays = { 198 | continents: [ 199 | "all", 200 | "asia", 201 | "north-america", 202 | "south-america", 203 | "europe", 204 | "africa", 205 | ], 206 | sorting: [ 207 | "alphabetical", 208 | "alphabetical-reversed", 209 | "population", 210 | "population-reversed", 211 | ], 212 | }; 213 | 214 | const animate = (callback) => { 215 | if (!document.startViewTransition) return callback(); 216 | 217 | flushSync(() => { 218 | document.startViewTransition(callback); 219 | }); 220 | }; 221 | 222 | const styles = css` 223 | body { 224 | background: rgba(0, 0, 0, 0.01); 225 | } 226 | 227 | ::view-transition-group(.card) { 228 | animation-duration: 0.8s; 229 | } 230 | `; 231 | 232 | const Controls = styled.div` 233 | padding: 2rem; 234 | display: flex; 235 | gap: 1rem; 236 | justify-content: center; 237 | `; 238 | 239 | const Item = styled.div` 240 | width: 18rem; 241 | `; 242 | 243 | const List = styled.ul` 244 | list-style: none; 245 | display: grid; 246 | gap: 1rem; 247 | grid-template-columns: repeat(6, 1fr); 248 | max-width: 60rem; 249 | margin: 0 auto; 250 | padding: 0; 251 | `; 252 | 253 | const Block = styled.li` 254 | height: 8rem; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | flex-direction: column; 259 | text-align: center; 260 | view-transition-class: card; 261 | `; 262 | 263 | const Image = styled.img` 264 | height: 5rem; 265 | width: 100%; 266 | object-fit: cover; 267 | border-bottom: 1px solid #ccc; 268 | `; 269 | 270 | const Label = styled.div` 271 | font-size: 1rem; 272 | flex-grow: 1; 273 | line-height: 1; 274 | padding-top: 1rem; 275 | font-size: 0.8rem; 276 | `; 277 | 278 | export const App = () => { 279 | const [sorting, setSorting] = useState("alphabetical"); 280 | const [continent, setContinent] = useState("all"); 281 | const [search, setSearch] = useState(""); 282 | 283 | const handleSorting = (event) => { 284 | const { value } = event.target; 285 | animate(() => setSorting(value)); 286 | }; 287 | 288 | const handleSearch = (event) => { 289 | const { value } = event.target; 290 | animate(() => setSearch(value || "")); 291 | }; 292 | 293 | const handleContinent = (event) => { 294 | const { value } = event.target; 295 | animate(() => setContinent(value)); 296 | }; 297 | 298 | const filtered = data.filter((country) => { 299 | if ( 300 | search !== "" && 301 | !country.name 302 | .toLowerCase() 303 | .includes(search.toLowerCase()) 304 | ) { 305 | return false; 306 | } 307 | 308 | if ( 309 | continent !== "all" && 310 | country.continent !== continent 311 | ) { 312 | return false; 313 | } 314 | 315 | return true; 316 | }); 317 | 318 | const sorted = filtered.sort((a, b) => { 319 | if (sorting === "alphabetical") { 320 | return a.name.localeCompare(b.name); 321 | } 322 | 323 | if (sorting === "alphabetical-reversed") { 324 | return b.name.localeCompare(a.name); 325 | } 326 | 327 | if (sorting === "population") { 328 | return a.population - b.population; 329 | } 330 | 331 | if (sorting === "population-reversed") { 332 | return b.population - a.population; 333 | } 334 | 335 | return 0; 336 | }); 337 | 338 | return ( 339 | 340 | 341 | 342 | 343 | 350 | 351 | 352 | 353 | 354 | 355 | Continent 356 | 357 | 358 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | Sorting 378 | 379 | 380 | 393 | 394 | 395 | 396 | 397 | 398 | {sorted.map(({ id, flag, name }) => { 399 | return ( 400 | 407 | 408 | 409 | 410 | ); 411 | })} 412 | 413 | 414 | ); 415 | }; 416 | 417 | export default App; 418 | -------------------------------------------------------------------------------- /examples/filtering-react/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/filtering-react/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/filtering-ssg/build.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { url } from "inspector"; 3 | 4 | /** 5 | * @typedef {'asia' | 'north-america' | 'europe' | 'africa' | 'south-america'} Continent 6 | */ 7 | 8 | /** 9 | * @typedef {'alphabetical' | 'alphabetical-reversed' | 'population' | 'population-reversed'} Sorting 10 | */ 11 | 12 | /** 13 | * @typedef {object} Country 14 | * @prop {string} id 15 | * @prop {string} flag 16 | * @prop {string} name 17 | * @prop {number} population 18 | * @prop {Continent} continent 19 | */ 20 | 21 | /** 22 | * @typedef {object} Options 23 | * @prop {Sorting} sorting 24 | * @prop {'all' | Continent} continent 25 | */ 26 | 27 | /** 28 | * @type {object} 29 | * @prop {Sorting[]} sorting 30 | * @prop {('all' | Continent)[]} continents 31 | */ 32 | const arrays = { 33 | continents: [ 34 | "all", 35 | "asia", 36 | "north-america", 37 | "south-america", 38 | "europe", 39 | "africa", 40 | ], 41 | sorting: [ 42 | "alphabetical", 43 | "alphabetical-reversed", 44 | "population", 45 | "population-reversed", 46 | ], 47 | }; 48 | 49 | /** 50 | * @type {Country[]} 51 | */ 52 | const data = [ 53 | { 54 | id: "bd", 55 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/bd.svg", 56 | name: "Bangladesh", 57 | population: 168000000, 58 | continent: "asia", 59 | }, 60 | { 61 | id: "br", 62 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/br.svg", 63 | name: "Brazil", 64 | population: 214000000, 65 | continent: "south-america", 66 | }, 67 | { 68 | id: "cn", 69 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cn.svg", 70 | name: "China", 71 | population: 1410000000, 72 | continent: "asia", 73 | }, 74 | { 75 | id: "cd", 76 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cd.svg", 77 | name: "Democratic Republic of the Congo", 78 | population: 96000000, 79 | continent: "africa", 80 | }, 81 | { 82 | id: "eg", 83 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/eg.svg", 84 | name: "Egypt", 85 | population: 109000000, 86 | continent: "africa", 87 | }, 88 | { 89 | id: "et", 90 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/et.svg", 91 | name: "Ethiopia", 92 | population: 117000000, 93 | continent: "africa", 94 | }, 95 | { 96 | id: "de", 97 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/de.svg", 98 | name: "Germany", 99 | population: 84000000, 100 | continent: "europe", 101 | }, 102 | { 103 | id: "in", 104 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/in.svg", 105 | name: "India", 106 | population: 1370000000, 107 | continent: "asia", 108 | }, 109 | { 110 | id: "id", 111 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/id.svg", 112 | name: "Indonesia", 113 | population: 276000000, 114 | continent: "asia", 115 | }, 116 | { 117 | id: "ir", 118 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ir.svg", 119 | name: "Iran", 120 | population: 85000000, 121 | continent: "asia", 122 | }, 123 | { 124 | id: "jp", 125 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/jp.svg", 126 | name: "Japan", 127 | population: 125000000, 128 | continent: "asia", 129 | }, 130 | { 131 | id: "mx", 132 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/mx.svg", 133 | name: "Mexico", 134 | population: 126000000, 135 | continent: "north-america", 136 | }, 137 | { 138 | id: "ng", 139 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ng.svg", 140 | name: "Nigeria", 141 | population: 211000000, 142 | continent: "africa", 143 | }, 144 | { 145 | id: "pk", 146 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/pk.svg", 147 | name: "Pakistan", 148 | population: 229000000, 149 | continent: "asia", 150 | }, 151 | { 152 | id: "ph", 153 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ph.svg", 154 | name: "Philippines", 155 | population: 113000000, 156 | continent: "asia", 157 | }, 158 | { 159 | id: "ru", 160 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ru.svg", 161 | name: "Russia", 162 | population: 146000000, 163 | continent: "europe", 164 | }, 165 | { 166 | id: "th", 167 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/th.svg", 168 | name: "Thailand", 169 | population: 70000000, 170 | continent: "asia", 171 | }, 172 | { 173 | id: "tr", 174 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/tr.svg", 175 | name: "Turkey", 176 | population: 86000000, 177 | continent: "europe", 178 | }, 179 | { 180 | id: "us", 181 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/us.svg", 182 | name: "United States", 183 | population: 332000000, 184 | continent: "north-america", 185 | }, 186 | { 187 | id: "vn", 188 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/vn.svg", 189 | name: "Vietnam", 190 | population: 99000000, 191 | continent: "asia", 192 | }, 193 | ]; 194 | 195 | /** 196 | * 197 | * @param {Options} options 198 | * @returns {string} 199 | */ 200 | const createHtml = (options) => { 201 | const { continent, sorting } = options; 202 | 203 | const filtered = data.filter((country) => { 204 | if ( 205 | continent !== "all" && 206 | country.continent !== continent 207 | ) { 208 | return false; 209 | } 210 | 211 | return true; 212 | }); 213 | 214 | const sorted = filtered.sort((a, b) => { 215 | if (sorting === "alphabetical") { 216 | return a.name.localeCompare(b.name); 217 | } 218 | 219 | if (sorting === "alphabetical-reversed") { 220 | return b.name.localeCompare(a.name); 221 | } 222 | 223 | if (sorting === "population") { 224 | return a.population - b.population; 225 | } 226 | 227 | if (sorting === "population-reversed") { 228 | return b.population - a.population; 229 | } 230 | 231 | return 0; 232 | }); 233 | 234 | const list = sorted 235 | .map(({ id, flag, name }) => { 236 | return ` 237 |
  • 238 | United States Flag 244 | ${name} 245 |
  • 246 | `; 247 | }) 248 | .join(""); 249 | 250 | const links = { 251 | continents: { 252 | all: `./all-${sorting}.html`, 253 | asia: `./asia-${sorting}.html`, 254 | "north-america": `./north-america-${sorting}.html`, 255 | "south-america": `./south-america-${sorting}.html`, 256 | europe: `./europe-${sorting}.html`, 257 | africa: `./africa-${sorting}.html`, 258 | }, 259 | sorting: { 260 | alphabetical: `./${continent}-alphabetical.html`, 261 | "alphabetical-reversed": `./${continent}-alphabetical-reversed.html`, 262 | population: `./${continent}-population.html`, 263 | "population-reversed": `./${continent}-population-reversed.html`, 264 | }, 265 | }; 266 | 267 | const prefetch = [ 268 | ...Object.values(links.continents) 269 | .map((url) => { 270 | return ``; 271 | }) 272 | .join(""), 273 | ...Object.values(links.continents) 274 | .map((url) => { 275 | return ``; 276 | }) 277 | .join(""), 278 | ].join(""); 279 | 280 | return ` 281 | 282 | 283 | 284 | 285 | 289 | 290 | 291 | ${prefetch} 292 | 293 | 294 |
    Continent
    295 | ${Object.entries(links.continents).map( 296 | ([label, url]) => { 297 | return `${label}`; 298 | } 299 | )} 300 | 301 |
    Sorting
    302 | ${Object.entries(links.sorting).map( 303 | ([label, url]) => { 304 | return `${label}`; 305 | } 306 | )} 307 | 310 | 311 | 312 | `; 313 | }; 314 | 315 | arrays.continents.forEach((continent) => { 316 | arrays.sorting.forEach((sorting) => { 317 | fs.writeFileSync( 318 | `./${continent}-${sorting}.html`, 319 | createHtml({ continent, sorting }) 320 | ); 321 | }); 322 | }); 323 | 324 | fs.writeFileSync( 325 | "./index.html", 326 | createHtml({ continent: "all", sorting: "alphabetical" }) 327 | ); 328 | -------------------------------------------------------------------------------- /examples/filtering-ssg/style.css: -------------------------------------------------------------------------------- 1 | @view-transition { 2 | navigation: auto; 3 | } 4 | -------------------------------------------------------------------------------- /examples/filtering-ssr/server.mjs: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import url from "url"; 3 | 4 | const data = [ 5 | { 6 | id: "bd", 7 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/bd.svg", 8 | name: "Bangladesh", 9 | population: 168000000, 10 | continent: "asia", 11 | }, 12 | { 13 | id: "br", 14 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/br.svg", 15 | name: "Brazil", 16 | population: 214000000, 17 | continent: "south-america", 18 | }, 19 | { 20 | id: "cn", 21 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cn.svg", 22 | name: "China", 23 | population: 1410000000, 24 | continent: "asia", 25 | }, 26 | { 27 | id: "cd", 28 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/cd.svg", 29 | name: "Democratic Republic of the Congo", 30 | population: 96000000, 31 | continent: "africa", 32 | }, 33 | { 34 | id: "eg", 35 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/eg.svg", 36 | name: "Egypt", 37 | population: 109000000, 38 | continent: "africa", 39 | }, 40 | { 41 | id: "et", 42 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/et.svg", 43 | name: "Ethiopia", 44 | population: 117000000, 45 | continent: "africa", 46 | }, 47 | { 48 | id: "de", 49 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/de.svg", 50 | name: "Germany", 51 | population: 84000000, 52 | continent: "europe", 53 | }, 54 | { 55 | id: "in", 56 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/in.svg", 57 | name: "India", 58 | population: 1370000000, 59 | continent: "asia", 60 | }, 61 | { 62 | id: "id", 63 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/id.svg", 64 | name: "Indonesia", 65 | population: 276000000, 66 | continent: "asia", 67 | }, 68 | { 69 | id: "ir", 70 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ir.svg", 71 | name: "Iran", 72 | population: 85000000, 73 | continent: "asia", 74 | }, 75 | { 76 | id: "jp", 77 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/jp.svg", 78 | name: "Japan", 79 | population: 125000000, 80 | continent: "asia", 81 | }, 82 | { 83 | id: "mx", 84 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/mx.svg", 85 | name: "Mexico", 86 | population: 126000000, 87 | continent: "north-america", 88 | }, 89 | { 90 | id: "ng", 91 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ng.svg", 92 | name: "Nigeria", 93 | population: 211000000, 94 | continent: "africa", 95 | }, 96 | { 97 | id: "pk", 98 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/pk.svg", 99 | name: "Pakistan", 100 | population: 229000000, 101 | continent: "asia", 102 | }, 103 | { 104 | id: "ph", 105 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ph.svg", 106 | name: "Philippines", 107 | population: 113000000, 108 | continent: "asia", 109 | }, 110 | { 111 | id: "ru", 112 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/ru.svg", 113 | name: "Russia", 114 | population: 146000000, 115 | continent: "europe", 116 | }, 117 | { 118 | id: "th", 119 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/th.svg", 120 | name: "Thailand", 121 | population: 70000000, 122 | continent: "asia", 123 | }, 124 | { 125 | id: "tr", 126 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/tr.svg", 127 | name: "Turkey", 128 | population: 86000000, 129 | continent: "europe", 130 | }, 131 | { 132 | id: "us", 133 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/us.svg", 134 | name: "United States", 135 | population: 332000000, 136 | continent: "north-america", 137 | }, 138 | { 139 | id: "vn", 140 | flag: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/vn.svg", 141 | name: "Vietnam", 142 | population: 99000000, 143 | continent: "asia", 144 | }, 145 | ]; 146 | 147 | const createHtml = (options) => { 148 | const { continent, sorting, search } = options; 149 | 150 | const filtered = data.filter((country) => { 151 | if ( 152 | search !== "" && 153 | !country.name.toLowerCase().includes(search.toLowerCase()) 154 | ) { 155 | return false; 156 | } 157 | 158 | if (continent !== "all" && country.continent !== continent) { 159 | return false; 160 | } 161 | 162 | return true; 163 | }); 164 | 165 | const sorted = filtered.sort((a, b) => { 166 | if (sorting === "alphabetical") { 167 | return a.name.localeCompare(b.name); 168 | } 169 | 170 | if (sorting === "alphabetical-reversed") { 171 | return b.name.localeCompare(a.name); 172 | return b.name.localeCompare(a.name); 173 | } 174 | 175 | if (sorting === "population") { 176 | return a.population - b.population; 177 | } 178 | 179 | if (sorting === "population-reversed") { 180 | return b.population - a.population; 181 | } 182 | 183 | return 0; 184 | }); 185 | 186 | const list = sorted 187 | .map(({ id, flag, name }) => { 188 | return ` 189 |
  • 190 | United States Flag 196 | ${name} 197 |
  • 198 | `; 199 | }) 200 | .join(""); 201 | 202 | return /* html */ ` 203 | 204 | 205 | 206 | 207 | 211 | 212 | 217 | 218 | 219 |
    220 | 224 | 225 | Continents 226 | 227 | 235 | 236 | Sorting 237 | 238 | 251 | 252 | 253 |
    254 | 255 | 258 | 259 | 260 | `; 261 | }; 262 | 263 | const server = http.createServer((req, res) => { 264 | const parsed = new URL(req.url, "http://localhost:3000/"); 265 | 266 | const continent = parsed.searchParams.get("continent") || "all"; 267 | const sorting = parsed.searchParams.get("sorting") || "alphabetical"; 268 | const search = parsed.searchParams.get("search") || ""; 269 | 270 | res.writeHead(200, { "Content-Type": "text/html" }); 271 | res.end(createHtml({ continent, sorting, search })); 272 | }); 273 | 274 | server.listen(3000, () => { 275 | console.log("Server running at http://localhost:3000/"); 276 | }); 277 | -------------------------------------------------------------------------------- /examples/filtering-ssr/style.css: -------------------------------------------------------------------------------- 1 | @view-transition { 2 | navigation: auto; 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "as", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.8.2", 14 | "astro": "^4.12.2", 15 | "date-fns": "^3.6.0", 16 | "typescript": "^5.5.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/details.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import content from "../data/content.json"; 3 | import genresRef from "../data/genres.json"; 4 | import { format } from "date-fns"; 5 | 6 | export const getStaticPaths = () => { 7 | return content.map((item) => ({ 8 | params: { id: item.id }, 9 | })); 10 | }; 11 | 12 | const { id } = Astro.props; 13 | const match = content.find((item) => item.id === id); 14 | 15 | if (!match) { 16 | Astro.meta({ status: 404 }); 17 | } 18 | 19 | const { description, image, genres, seasons, date } = match as any; 20 | --- 21 | 22 | 76 | 77 |
    78 |
    79 | 87 |
    88 |
    89 |

    90 | {description} 91 |

    92 | 93 |
    Seasons: {seasons}
    94 |
    Released: {format(date, "dd MMMM yyyy")}
    95 | 96 | 107 |
    108 |
    109 | -------------------------------------------------------------------------------- /src/components/genres.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import genresRef from "../data/genres.json"; 3 | const { active } = Astro.props; 4 | const genre = (genresRef as any)[active]; 5 | --- 6 | 7 | 35 | 36 |

    {genre}

    37 | 38 | 55 | -------------------------------------------------------------------------------- /src/components/shell.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { active, header } = Astro.props; 3 | --- 4 | 5 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | { 75 | header !== false && ( 76 |
    77 |

    Streaming App

    78 | 79 | 90 |
    91 | ) 92 | } 93 | 94 |
    95 | 96 |
    97 | 98 | 99 | -------------------------------------------------------------------------------- /src/components/shows.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import content from "../data/content.json"; 3 | const { filter, limit } = Astro.props; 4 | let count = 0; 5 | 6 | const filtered = 7 | filter === "0" 8 | ? content 9 | : content.filter((item) => { 10 | if (limit && count >= limit) return false; 11 | const match = item.genres.includes(parseInt(filter)); 12 | if (!match) return false; 13 | count += 1; 14 | return true; 15 | }); 16 | --- 17 | 18 | 69 | 70 |
    71 | { 72 | filtered.map((item) => ( 73 | 80 | 89 | 90 |

    96 | {item.title} 97 |

    98 |
    99 | )) 100 | } 101 |
    102 | -------------------------------------------------------------------------------- /src/data/content.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "b8ddbcbe-6860-42cd-9ad0-106b5028fa75", 4 | "description": "A chemistry teacher diagnosed with inoperable lung cancer turns to manufacturing and selling methamphetamine with a former student in order to secure his family's future.", 5 | "title": "Breaking Bad", 6 | "image": "https://epic-stream-api.netlify.app/assets/shows/b8ddbcbe-6860-42cd-9ad0-106b5028fa75.jpg", 7 | "genres": [1, 2, 3], 8 | "seasons": 5, 9 | "date": "2013-09-28T22:00:00.000Z" 10 | }, 11 | { 12 | "id": "4a421308-dc18-4a04-85ce-766e9a8896cb", 13 | "description": "David Attenborough returns with a new wildlife documentary that shows life in a variety of habitats.", 14 | "title": "Planet Earth II", 15 | "image": "https://epic-stream-api.netlify.app/assets/shows/4a421308-dc18-4a04-85ce-766e9a8896cb.jpg", 16 | "genres": [4, 5], 17 | "seasons": 1, 18 | "date": "2017-03-24T22:00:00.000Z" 19 | }, 20 | { 21 | "id": "713e9dec-4676-42d3-957e-04d80c700ec6", 22 | "description": "A documentary series on the wildlife found on Earth. Each episode covers a different habitat: deserts, mountains, deep oceans, shallow seas, forests, caves, polar regions, fresh water, plains and jungles. Narrated by David Attenborough.", 23 | "title": "Planet Earth", 24 | "image": "https://epic-stream-api.netlify.app/assets/shows/713e9dec-4676-42d3-957e-04d80c700ec6.jpg", 25 | "genres": [4, 5], 26 | "seasons": 1, 27 | "date": "2007-02-10T22:00:00.000Z" 28 | }, 29 | { 30 | "id": "099f7ced-f2f6-4f7f-ba67-8f5b9b4137cf", 31 | "description": "The story of Easy Company of the U.S. Army 101st Airborne Division and their mission in World War II Europe, from Operation Overlord to V-J Day.", 32 | "title": "Band of Brothers", 33 | "image": "https://epic-stream-api.netlify.app/assets/shows/099f7ced-f2f6-4f7f-ba67-8f5b9b4137cf.jpg", 34 | "genres": [2, 6, 7], 35 | "seasons": 1, 36 | "date": "2001-11-03T22:00:00.000Z" 37 | }, 38 | { 39 | "id": "485a12bb-39b8-40d2-a8cc-59d9060baad5", 40 | "description": "In April 1986, an explosion at the Chernobyl nuclear power plant in the Union of Soviet Socialist Republics becomes one of the world's worst man-made catastrophes.", 41 | "title": "Chernobyl", 42 | "image": "https://epic-stream-api.netlify.app/assets/shows/485a12bb-39b8-40d2-a8cc-59d9060baad5.jpg", 43 | "genres": [2, 7, 3], 44 | "seasons": 1, 45 | "date": "2019-06-02T22:00:00.000Z" 46 | }, 47 | { 48 | "id": "7334e880-97bc-45b8-9af3-2c5d59e224f4", 49 | "description": "The Baltimore drug scene, as seen through the eyes of drug dealers and law enforcement.", 50 | "title": "The Wire", 51 | "image": "https://epic-stream-api.netlify.app/assets/shows/7334e880-97bc-45b8-9af3-2c5d59e224f4.jpg", 52 | "genres": [1, 2, 3], 53 | "seasons": 5, 54 | "date": "2008-03-08T22:00:00.000Z" 55 | }, 56 | { 57 | "id": "6896ea1e-f8dc-44d6-9ed1-ae1b08b81bbb", 58 | "description": "In a war-torn world of elemental magic, a young boy reawakens to undertake a dangerous mystic quest to fulfill his destiny as the Avatar, and bring peace to the world.", 59 | "title": "Avatar: The Last Airbender", 60 | "image": "https://epic-stream-api.netlify.app/assets/shows/6896ea1e-f8dc-44d6-9ed1-ae1b08b81bbb.jpg", 61 | "genres": [8, 9, 10], 62 | "seasons": 3, 63 | "date": "2008-07-18T22:00:00.000Z" 64 | }, 65 | { 66 | "id": "449a6e7b-e780-4146-8637-1e01705ce756", 67 | "description": "David Attenborough returns to the world's oceans in this sequel to the acclaimed documentary filming rare and unusual creatures of the deep, as well as documenting the problems our oceans face.", 68 | "title": "Blue Planet II", 69 | "image": "https://epic-stream-api.netlify.app/assets/shows/449a6e7b-e780-4146-8637-1e01705ce756.jpg", 70 | "genres": [4, 5], 71 | "seasons": 1, 72 | "date": "2017-12-09T22:00:00.000Z" 73 | }, 74 | { 75 | "id": "9a552b3c-3ef2-4bd5-8281-8ac477647ee1", 76 | "description": "New Jersey mob boss Tony Soprano deals with personal and professional issues in his home and business life that affect his mental state, leading him to seek professional psychiatric counseling.", 77 | "title": "The Sopranos", 78 | "image": "https://epic-stream-api.netlify.app/assets/shows/9a552b3c-3ef2-4bd5-8281-8ac477647ee1.jpg", 79 | "genres": [1, 2], 80 | "seasons": 6, 81 | "date": "2007-06-09T22:00:00.000Z" 82 | }, 83 | { 84 | "id": "d2313809-dc61-418e-9737-3d04b88afbdc", 85 | "description": "An exploration of our discovery of the laws of nature and coordinates in space and time.", 86 | "title": "Cosmos: A Spacetime Odyssey", 87 | "image": "https://epic-stream-api.netlify.app/assets/shows/d2313809-dc61-418e-9737-3d04b88afbdc.jpg", 88 | "genres": [4, 11], 89 | "seasons": 1, 90 | "date": "2014-06-07T22:00:00.000Z" 91 | }, 92 | { 93 | "id": "1be4f022-993e-4abd-acaa-f4c9409101a0", 94 | "description": "Astronomer Carl Sagan leads us on an engaging guided tour of the various elements and cosmological theories of the universe.", 95 | "title": "Cosmos", 96 | "image": "https://epic-stream-api.netlify.app/assets/shows/1be4f022-993e-4abd-acaa-f4c9409101a0.jpg", 97 | "genres": [4, 11], 98 | "seasons": 1, 99 | "date": "1980-12-20T22:00:00.000Z" 100 | }, 101 | { 102 | "id": "af40f1cd-f3fa-48a5-ab89-189e18fa3d88", 103 | "description": "Explores and unravels the mystery of how and why animals migrate, showing some of the most dramatic and compelling stories in the natural world through spectacular and innovative cinematography.", 104 | "title": "Our Planet", 105 | "image": "https://epic-stream-api.netlify.app/assets/shows/af40f1cd-f3fa-48a5-ab89-189e18fa3d88.jpg", 106 | "genres": [4, 5], 107 | "seasons": 2, 108 | "date": "2023-06-13T22:00:00.000Z" 109 | }, 110 | { 111 | "id": "66ff16fb-8df3-41a8-b463-142ff61f5ecc", 112 | "description": "Nine noble families fight for control over the lands of Westeros, while an ancient enemy returns after being dormant for a millennia.", 113 | "title": "Game of Thrones", 114 | "image": "https://epic-stream-api.netlify.app/assets/shows/66ff16fb-8df3-41a8-b463-142ff61f5ecc.jpg", 115 | "genres": [9, 10, 2], 116 | "seasons": 8, 117 | "date": "2019-05-18T22:00:00.000Z" 118 | }, 119 | { 120 | "id": "ef96bcd7-e0cd-455c-bac2-dea4142a0c48", 121 | "description": "A groundbreaking 26-part documentary series narrated by the actor Laurence Olivier about the deadliest conflict in history, World War II.", 122 | "title": "The World at War", 123 | "image": "https://epic-stream-api.netlify.app/assets/shows/ef96bcd7-e0cd-455c-bac2-dea4142a0c48.jpg", 124 | "genres": [4, 7, 6], 125 | "seasons": 1, 126 | "date": "1974-05-07T22:00:00.000Z" 127 | }, 128 | { 129 | "id": "fce30d2a-37c0-493f-a85c-e2cffc950794", 130 | "description": "The slice-of-life adventures of an Australian Blue Heeler Cattle Dog puppy as she has fun with her family and friends in everyday situations.", 131 | "title": "Bluey", 132 | "image": "https://epic-stream-api.netlify.app/assets/shows/fce30d2a-37c0-493f-a85c-e2cffc950794.jpg", 133 | "genres": [8, 12], 134 | "seasons": 1, 135 | "date": "2019-04-23T22:00:00.000Z" 136 | }, 137 | { 138 | "id": "92b8986d-f7fe-446f-a196-17057b24cffe", 139 | "description": "Two brothers search for a Philosopher's Stone after an attempt to revive their deceased mother goes awry and leaves them in damaged physical forms.", 140 | "title": "Fullmetal Alchemist: Brotherhood", 141 | "image": "https://epic-stream-api.netlify.app/assets/shows/92b8986d-f7fe-446f-a196-17057b24cffe.jpg", 142 | "genres": [8, 9, 10], 143 | "seasons": 1, 144 | "date": "2010-03-27T22:00:00.000Z" 145 | }, 146 | { 147 | "id": "e68c5976-e946-4d43-ba94-092764b24634", 148 | "description": "Charting the rise of the 1990s Chicago Bulls, led by Michael Jordan, one of the most notable dynasties in sports history.", 149 | "title": "The Last Dance", 150 | "image": "https://epic-stream-api.netlify.app/assets/shows/e68c5976-e946-4d43-ba94-092764b24634.jpg", 151 | "genres": [4, 13, 14], 152 | "seasons": 1, 153 | "date": "2020-05-16T22:00:00.000Z" 154 | }, 155 | { 156 | "id": "7938c24e-dbe5-4b53-97c8-4670aae17d34", 157 | "description": "David Attenborough's legendary BBC crew explains and shows wildlife all over planet earth. From giving an overview of the challenges facing life to hunting the deep sea and various major evolutionary groups of creatures.", 158 | "title": "Life", 159 | "image": "https://epic-stream-api.netlify.app/assets/shows/7938c24e-dbe5-4b53-97c8-4670aae17d34.jpg", 160 | "genres": [4, 5], 161 | "seasons": 1, 162 | "date": "2009-01-10T22:00:00.000Z" 163 | }, 164 | { 165 | "id": "80859ab5-46b4-461e-b496-cc2e8bccad57", 166 | "description": "Ordinary people find themselves in extraordinarily astounding situations, which they each try to solve in a remarkable manner.", 167 | "title": "The Twilight Zone", 168 | "image": "https://epic-stream-api.netlify.app/assets/shows/80859ab5-46b4-461e-b496-cc2e8bccad57.jpg", 169 | "genres": [2, 15, 16], 170 | "seasons": 5, 171 | "date": "1964-06-18T22:00:00.000Z" 172 | }, 173 | { 174 | "id": "c90488a8-cde2-4d01-901a-522816100fd7", 175 | "description": "The quirky spin on Conan Doyle's iconic sleuth pitches him as a \"high-functioning sociopath\" in modern-day London. Assisting him in his investigations: Afghanistan War vet John Watson, who's introduced to Holmes by a mutual acquaintance.", 176 | "title": "Sherlock", 177 | "image": "https://epic-stream-api.netlify.app/assets/shows/c90488a8-cde2-4d01-901a-522816100fd7.jpg", 178 | "genres": [1, 2, 17], 179 | "seasons": 4, 180 | "date": "2017-01-14T22:00:00.000Z" 181 | }, 182 | { 183 | "id": "f534959a-ac86-494a-bae2-33a4edd525d6", 184 | "description": "A comprehensive history of the United States' involvement in the bitterly divisive armed conflict in Southeast Asia.", 185 | "title": "The Vietnam War", 186 | "image": "https://epic-stream-api.netlify.app/assets/shows/f534959a-ac86-494a-bae2-33a4edd525d6.jpg", 187 | "genres": [4, 7, 6], 188 | "seasons": 1, 189 | "date": "2017-09-27T22:00:00.000Z" 190 | }, 191 | { 192 | "id": "da992665-d1c7-47f1-84c1-7df1d3bedbab", 193 | "description": "The Dark Knight battles crime in Gotham City with occasional help from Robin and Batgirl.", 194 | "title": "Batman: The Animated Series", 195 | "image": "https://epic-stream-api.netlify.app/assets/shows/da992665-d1c7-47f1-84c1-7df1d3bedbab.jpg", 196 | "genres": [8, 9, 10], 197 | "seasons": 4, 198 | "date": "1995-09-14T22:00:00.000Z" 199 | }, 200 | { 201 | "id": "9b80fe01-7683-4537-86e0-a169f368cac9", 202 | "description": "After his hometown is destroyed and his mother is killed, young Eren Jaeger vows to cleanse the earth of the giant humanoid Titans that have brought humanity to the brink of extinction.", 203 | "title": "Attack on Titan", 204 | "image": "https://epic-stream-api.netlify.app/assets/shows/9b80fe01-7683-4537-86e0-a169f368cac9.jpg", 205 | "genres": [8, 9, 2], 206 | "seasons": 1, 207 | "date": "2013-09-28T22:00:00.000Z" 208 | }, 209 | { 210 | "id": "1164b939-e057-40d9-940c-c13e6e5b002b", 211 | "description": "The rise and fall of Harshad Mehta, a stockbroker who single-handedly took the stock market to great heights, is depicted.", 212 | "title": "Scam 1992: The Harshad Mehta Story", 213 | "image": "https://epic-stream-api.netlify.app/assets/shows/1164b939-e057-40d9-940c-c13e6e5b002b.jpg", 214 | "genres": [13, 1, 2], 215 | "seasons": 1, 216 | "date": "2020-10-08T22:00:00.000Z" 217 | }, 218 | { 219 | "id": "94ff52dc-810c-4d37-8d00-5b4945be44a0", 220 | "description": "A mockumentary on a group of typical office workers, where the workday consists of ego clashes, inappropriate behavior, and tedium.", 221 | "title": "The Office", 222 | "image": "https://epic-stream-api.netlify.app/assets/shows/94ff52dc-810c-4d37-8d00-5b4945be44a0.jpg", 223 | "genres": [18], 224 | "seasons": 9, 225 | "date": "2013-05-15T22:00:00.000Z" 226 | }, 227 | { 228 | "id": "51d78f3c-aa96-48eb-8096-0e21f86470dc", 229 | "description": "Mammoth series, five years in the making, taking a look at the rich tapestry of life in the world's oceans.", 230 | "title": "The Blue Planet", 231 | "image": "https://epic-stream-api.netlify.app/assets/shows/51d78f3c-aa96-48eb-8096-0e21f86470dc.jpg", 232 | "genres": [4, 5], 233 | "seasons": 1, 234 | "date": "2001-10-30T22:00:00.000Z" 235 | }, 236 | { 237 | "id": "3baeeeee-46d6-44d1-8741-09973655240e", 238 | "description": "The trials and tribulations of criminal lawyer Jimmy McGill in the years leading up to his fateful run-in with Walter White and Jesse Pinkman.", 239 | "title": "Better Call Saul", 240 | "image": "https://epic-stream-api.netlify.app/assets/shows/3baeeeee-46d6-44d1-8741-09973655240e.jpg", 241 | "genres": [1, 2], 242 | "seasons": 6, 243 | "date": "2022-08-14T22:00:00.000Z" 244 | }, 245 | { 246 | "id": "5f1ed501-b892-4911-aa76-04dfd6986da9", 247 | "description": "A cinematic experience bringing you the most amazing human stories in the world. Humans and wildlife surviving in the most extreme environments on Earth", 248 | "title": "Human Planet", 249 | "image": "https://epic-stream-api.netlify.app/assets/shows/5f1ed501-b892-4911-aa76-04dfd6986da9.jpg", 250 | "genres": [4, 5], 251 | "seasons": 1, 252 | "date": "2011-03-02T22:00:00.000Z" 253 | }, 254 | { 255 | "id": "d1fa398c-67df-4d5a-9937-cbc39334328b", 256 | "description": "Five hundred years in the future, a renegade crew aboard a small spacecraft tries to survive as they travel the unknown parts of the galaxy and evade warring factions as well as authority agents out to get them.", 257 | "title": "Firefly", 258 | "image": "https://epic-stream-api.netlify.app/assets/shows/d1fa398c-67df-4d5a-9937-cbc39334328b.jpg", 259 | "genres": [10, 2, 19], 260 | "seasons": 1, 261 | "date": "2003-07-27T22:00:00.000Z" 262 | }, 263 | { 264 | "id": "41c8e37a-67d1-4cbc-bca7-cf1db95b472d", 265 | "description": "Focuses on life and the environment in both the Arctic and Antarctic.", 266 | "title": "Frozen Planet", 267 | "image": "https://epic-stream-api.netlify.app/assets/shows/41c8e37a-67d1-4cbc-bca7-cf1db95b472d.jpg", 268 | "genres": [4, 5], 269 | "seasons": 1, 270 | "date": "2012-04-07T22:00:00.000Z" 271 | }, 272 | { 273 | "id": "3cfa88c0-2b64-451e-9a38-20efa525b6e7", 274 | "description": "An intelligent high school student goes on a secret crusade to eliminate criminals from the world after discovering a notebook capable of killing anyone whose name is written into it.", 275 | "title": "Death Note", 276 | "image": "https://epic-stream-api.netlify.app/assets/shows/3cfa88c0-2b64-451e-9a38-20efa525b6e7.jpg", 277 | "genres": [8, 1, 2], 278 | "seasons": 1, 279 | "date": "2007-06-25T22:00:00.000Z" 280 | }, 281 | { 282 | "id": "461f7c3a-d7e1-4b9b-9df6-5505b63436c8", 283 | "description": "Comedy that follows two brothers from London's rough Peckham estate as they wheel and deal through a number of dodgy deals and search for the big score that'll make them millionaires.", 284 | "title": "Only Fools and Horses", 285 | "image": "https://epic-stream-api.netlify.app/assets/shows/461f7c3a-d7e1-4b9b-9df6-5505b63436c8.jpg", 286 | "genres": [18], 287 | "seasons": 9, 288 | "date": "2003-12-24T22:00:00.000Z" 289 | }, 290 | { 291 | "id": "6c7ad08e-b983-4d41-9d76-91969a619b81", 292 | "description": "Gon Freecss aspires to become a Hunter, an exceptional being capable of greatness. With his friends and his potential, he seeks out his father, who left him when he was younger.", 293 | "title": "Hunter x Hunter", 294 | "image": "https://epic-stream-api.netlify.app/assets/shows/6c7ad08e-b983-4d41-9d76-91969a619b81.jpg", 295 | "genres": [8, 9, 10], 296 | "seasons": 1, 297 | "date": "2017-05-06T22:00:00.000Z" 298 | }, 299 | { 300 | "id": "2c92f6f2-7150-4f6c-9a60-8d122ee2a144", 301 | "description": "A comprehensive survey of the American Civil War.", 302 | "title": "The Civil War", 303 | "image": "https://epic-stream-api.netlify.app/assets/shows/2c92f6f2-7150-4f6c-9a60-8d122ee2a144.jpg", 304 | "genres": [4, 7, 6], 305 | "seasons": 1, 306 | "date": "1990-09-26T22:00:00.000Z" 307 | }, 308 | { 309 | "id": "b0e3a2f0-2a1c-4c67-85a8-0443cf6cfb9d", 310 | "description": "The continuing misadventures of neurotic New York City stand-up comedian Jerry Seinfeld and his equally neurotic New York City friends.", 311 | "title": "Seinfeld", 312 | "image": "https://epic-stream-api.netlify.app/assets/shows/b0e3a2f0-2a1c-4c67-85a8-0443cf6cfb9d.jpg", 313 | "genres": [18], 314 | "seasons": 9, 315 | "date": "1998-05-13T22:00:00.000Z" 316 | }, 317 | { 318 | "id": "57b1ef26-a5d6-43e4-9cfd-a8433b49541b", 319 | "description": "In January 1969, The Beatles set out to write and record new songs for their first live show in more than two years, culminating in an impromptu concert atop their Savile Row studio.", 320 | "title": "The Beatles: Get Back", 321 | "image": "https://epic-stream-api.netlify.app/assets/shows/57b1ef26-a5d6-43e4-9cfd-a8433b49541b.jpg", 322 | "genres": [4, 20], 323 | "seasons": 1, 324 | "date": "2021-11-26T22:00:00.000Z" 325 | }, 326 | { 327 | "id": "11ecaa83-1f19-4010-8393-4594c4168cb7", 328 | "description": "Ten television drama films, each one based on one of the Ten Commandments.", 329 | "title": "The Decalogue", 330 | "image": "https://epic-stream-api.netlify.app/assets/shows/11ecaa83-1f19-4010-8393-4594c4168cb7.jpg", 331 | "genres": [2], 332 | "seasons": 1, 333 | "date": "1989-06-23T22:00:00.000Z" 334 | }, 335 | { 336 | "id": "a9db7efc-d7ce-4fca-9104-edf63cfe46ed", 337 | "description": "The futuristic misadventures and tragedies of an easygoing bounty hunter and his partners.", 338 | "title": "Cowboy Bebop", 339 | "image": "https://epic-stream-api.netlify.app/assets/shows/a9db7efc-d7ce-4fca-9104-edf63cfe46ed.jpg", 340 | "genres": [8, 9, 10], 341 | "seasons": 1, 342 | "date": "2001-11-24T22:00:00.000Z" 343 | }, 344 | { 345 | "id": "c52f04b0-21ff-4a12-b302-c69c49ed8d60", 346 | "description": "Twin siblings Dipper and Mabel Pines spend the summer at their great-uncle's tourist trap in the enigmatic Gravity Falls, Oregon.", 347 | "title": "Gravity Falls", 348 | "image": "https://epic-stream-api.netlify.app/assets/shows/c52f04b0-21ff-4a12-b302-c69c49ed8d60.jpg", 349 | "genres": [8, 10, 18], 350 | "seasons": 2, 351 | "date": "2016-02-14T22:00:00.000Z" 352 | }, 353 | { 354 | "id": "a8a31dd2-1433-4f4f-bb0c-10b187e05c3e", 355 | "description": "Nathan Fielder uses his business degree and life experiences to help real small businesses turn a profit. But because of his unorthodox approach, Nathan's genuine efforts to do good often draw real people into an experience far beyond what they signed up for.", 356 | "title": "Nathan for You", 357 | "image": "https://epic-stream-api.netlify.app/assets/shows/a8a31dd2-1433-4f4f-bb0c-10b187e05c3e.jpg", 358 | "genres": [18, 4], 359 | "seasons": 4, 360 | "date": "2017-11-08T22:00:00.000Z" 361 | }, 362 | { 363 | "id": "08ffbcd7-f8fd-45c3-a5b0-68584ab6141b", 364 | "description": "Former Daily Show host and correspondent John Oliver brings his persona to this weekly news satire program.", 365 | "title": "Last Week Tonight with John Oliver", 366 | "image": "https://epic-stream-api.netlify.app/assets/shows/08ffbcd7-f8fd-45c3-a5b0-68584ab6141b.jpg", 367 | "genres": [18, 21], 368 | "seasons": 1, 369 | "date": "2014-11-08T22:00:00.000Z" 370 | }, 371 | { 372 | "id": "5d94e184-74c6-4f5b-adf0-5b25db2a1c3f", 373 | "description": "Five teens from Harlem become trapped in a nightmare when they're falsely accused of a brutal attack in Central Park. Based on the true story.", 374 | "title": "When They See Us", 375 | "image": "https://epic-stream-api.netlify.app/assets/shows/5d94e184-74c6-4f5b-adf0-5b25db2a1c3f.jpg", 376 | "genres": [13, 1, 2], 377 | "seasons": 1, 378 | "date": "2019-05-30T22:00:00.000Z" 379 | }, 380 | { 381 | "id": "b6043654-ddf2-4554-b14c-e080d17ec5d8", 382 | "description": "The Roy family is known for controlling the biggest media and entertainment company in the world. However, their world changes when their father steps down from the company.", 383 | "title": "Succession", 384 | "image": "https://epic-stream-api.netlify.app/assets/shows/b6043654-ddf2-4554-b14c-e080d17ec5d8.jpg", 385 | "genres": [2], 386 | "seasons": 4, 387 | "date": "2023-05-27T22:00:00.000Z" 388 | }, 389 | { 390 | "id": "d9825e2c-1112-46f5-91dc-e6c60249558f", 391 | "description": "This six-part series traces the Second World War, from the rise of the Nazis to the surrender of the Japanese, with detailed portraits of key figures.", 392 | "title": "Apocalypse: The Second World War", 393 | "image": "https://epic-stream-api.netlify.app/assets/shows/d9825e2c-1112-46f5-91dc-e6c60249558f.jpg", 394 | "genres": [4, 7, 6], 395 | "seasons": 1, 396 | "date": "2009-09-21T22:00:00.000Z" 397 | }, 398 | { 399 | "id": "14804e0b-ecc7-4a61-a234-bd3ea5216fa2", 400 | "description": "Follows the personal and professional lives of six twenty to thirty year-old friends living in the Manhattan borough of New York City.", 401 | "title": "Friends", 402 | "image": "https://epic-stream-api.netlify.app/assets/shows/14804e0b-ecc7-4a61-a234-bd3ea5216fa2.jpg", 403 | "genres": [18, 23], 404 | "seasons": 10, 405 | "date": "2004-05-05T22:00:00.000Z" 406 | }, 407 | { 408 | "id": "d37ce52c-d2f1-4c37-8270-723b57af85cb", 409 | "description": "Africa, the world's wildest continent. David Attenborough takes us on an awe-inspiring journey through one of the most diverse places in the world. We visit deserts, savannas, and jungles and meet up with some of Africa's amazing wildlife.", 410 | "title": "Africa", 411 | "image": "https://epic-stream-api.netlify.app/assets/shows/d37ce52c-d2f1-4c37-8270-723b57af85cb.jpg", 412 | "genres": [4, 5], 413 | "seasons": 1, 414 | "date": "2013-02-05T22:00:00.000Z" 415 | }, 416 | { 417 | "id": "8fb5dbd8-1c1a-407a-97cd-7118243e8246", 418 | "description": "Five comedians are set tasks challenging their creativity and wit. The tasks are supervised by Alex Horne but the Taskmaster, Greg Davies, always has the final word.", 419 | "title": "Taskmaster", 420 | "image": "https://epic-stream-api.netlify.app/assets/shows/8fb5dbd8-1c1a-407a-97cd-7118243e8246.jpg", 421 | "genres": [18], 422 | "seasons": 1, 423 | "date": "2015-08-31T22:00:00.000Z" 424 | }, 425 | { 426 | "id": "e45e0d2e-1a6a-4719-84f8-946dc2a5df6d", 427 | "description": "A story of trials and tribulations of four young entrepreneurs who quit their day jobs in order to pursue their start up venture.", 428 | "title": "TVF Pitchers", 429 | "image": "https://epic-stream-api.netlify.app/assets/shows/e45e0d2e-1a6a-4719-84f8-946dc2a5df6d.jpg", 430 | "genres": [2], 431 | "seasons": 2, 432 | "date": "2022-12-22T22:00:00.000Z" 433 | }, 434 | { 435 | "id": "7a502af0-72e7-40fe-94f9-1f60cadde210", 436 | "description": "Five friends with big egos and small brains are the proprietors of an Irish pub in Philadelphia.", 437 | "title": "It's Always Sunny in Philadelphia", 438 | "image": "https://epic-stream-api.netlify.app/assets/shows/7a502af0-72e7-40fe-94f9-1f60cadde210.jpg", 439 | "genres": [18], 440 | "seasons": 1, 441 | "date": "2005-09-12T22:00:00.000Z" 442 | }, 443 | { 444 | "id": "5cd11307-b531-4be9-9ea3-7b4891ba357f", 445 | "description": "The original surreal sketch comedy showcase for the Monty Python troupe.", 446 | "title": "Monty Python's Flying Circus", 447 | "image": "https://epic-stream-api.netlify.app/assets/shows/5cd11307-b531-4be9-9ea3-7b4891ba357f.jpg", 448 | "genres": [18], 449 | "seasons": 4, 450 | "date": "1974-12-04T22:00:00.000Z" 451 | }, 452 | { 453 | "id": "a0705f9a-dec5-4da3-bf11-94a1a34e1d34", 454 | "description": "Inside the lives of staffers in the West Wing of the White House.", 455 | "title": "The West Wing", 456 | "image": "https://epic-stream-api.netlify.app/assets/shows/a0705f9a-dec5-4da3-bf11-94a1a34e1d34.jpg", 457 | "genres": [2], 458 | "seasons": 7, 459 | "date": "2006-05-13T22:00:00.000Z" 460 | }, 461 | { 462 | "id": "354426f7-e337-4131-9cbf-bfcfd4dcde51", 463 | "description": "A World War II German U-Boat crew have a terrifying patrol mission in the early days of the war.", 464 | "title": "Das Boot", 465 | "image": "https://epic-stream-api.netlify.app/assets/shows/354426f7-e337-4131-9cbf-bfcfd4dcde51.jpg", 466 | "genres": [2, 6], 467 | "seasons": 1, 468 | "date": "1985-03-02T22:00:00.000Z" 469 | }, 470 | { 471 | "id": "4899aa33-29b5-4f2a-b701-cbb521728eed", 472 | "description": "The life and times of Larry David and the predicaments he gets himself into with his friends and complete strangers.", 473 | "title": "Curb Your Enthusiasm", 474 | "image": "https://epic-stream-api.netlify.app/assets/shows/4899aa33-29b5-4f2a-b701-cbb521728eed.jpg", 475 | "genres": [18], 476 | "seasons": 11, 477 | "date": "2021-12-25T22:00:00.000Z" 478 | }, 479 | { 480 | "id": "de080c05-15f5-4f36-9f4e-1258872fa498", 481 | "description": "Monkey D. Luffy sets off on an adventure with his pirate crew in hopes of finding the greatest treasure ever, known as the \"One Piece.\"", 482 | "title": "One Piece", 483 | "image": "https://epic-stream-api.netlify.app/assets/shows/de080c05-15f5-4f36-9f4e-1258872fa498.jpg", 484 | "genres": [8, 9, 10], 485 | "seasons": 1, 486 | "date": "2000-11-28T22:00:00.000Z" 487 | }, 488 | { 489 | "id": "69b1306e-bb64-4fad-8e98-e32ee7782e8f", 490 | "description": "Hotel owner Basil Fawlty's incompetence, short fuse, and arrogance form a combination that ensures accidents and trouble are never far away.", 491 | "title": "Fawlty Towers", 492 | "image": "https://epic-stream-api.netlify.app/assets/shows/69b1306e-bb64-4fad-8e98-e32ee7782e8f.jpg", 493 | "genres": [18], 494 | "seasons": 2, 495 | "date": "1979-10-24T22:00:00.000Z" 496 | }, 497 | { 498 | "id": "68f0c5bd-c163-469f-a8a6-3aad0faf9bf9", 499 | "description": "BoJack Horseman was the star of the hit television show \"Horsin' Around\" in the '80s and '90s, but now he's washed up, living in Hollywood, complaining about everything, and wearing colorful sweaters.", 500 | "title": "BoJack Horseman", 501 | "image": "https://epic-stream-api.netlify.app/assets/shows/68f0c5bd-c163-469f-a8a6-3aad0faf9bf9.jpg", 502 | "genres": [8, 18, 2], 503 | "seasons": 6, 504 | "date": "2020-01-30T22:00:00.000Z" 505 | }, 506 | { 507 | "id": "8a8b7607-214d-441a-8e5b-d59fd5b0fdcd", 508 | "description": "While the arrival of wealthy gentlemen sends her marriage-minded mother into a frenzy, willful and opinionated Elizabeth Bennet matches wits with haughty Mr. Darcy.", 509 | "title": "Pride and Prejudice", 510 | "image": "https://epic-stream-api.netlify.app/assets/shows/8a8b7607-214d-441a-8e5b-d59fd5b0fdcd.jpg", 511 | "genres": [2, 23], 512 | "seasons": 1, 513 | "date": "1996-01-15T22:00:00.000Z" 514 | }, 515 | { 516 | "id": "5ea9a106-e290-49ac-a2c8-fab5ef413b89", 517 | "description": "A high school mathlete starts hanging out with a group of burnouts while her younger brother navigates his freshman year.", 518 | "title": "Freaks and Geeks", 519 | "image": "https://epic-stream-api.netlify.app/assets/shows/5ea9a106-e290-49ac-a2c8-fab5ef413b89.jpg", 520 | "genres": [18, 2], 521 | "seasons": 1, 522 | "date": "2000-07-07T22:00:00.000Z" 523 | }, 524 | { 525 | "id": "d33ca262-340d-4e48-bbb8-70ff086a2364", 526 | "description": "Stuck in the middle of World War I, Captain Edmund Blackadder does his best to escape the banality of the war.", 527 | "title": "Blackadder Goes Forth", 528 | "image": "https://epic-stream-api.netlify.app/assets/shows/d33ca262-340d-4e48-bbb8-70ff086a2364.jpg", 529 | "genres": [18, 7], 530 | "seasons": 1, 531 | "date": "1989-11-01T22:00:00.000Z" 532 | }, 533 | { 534 | "id": "62199cb2-14ad-4035-9060-3f8f93ab7f08", 535 | "description": "An idiosyncratic FBI agent investigates the murder of a young woman in the even more idiosyncratic town of Twin Peaks.", 536 | "title": "Twin Peaks", 537 | "image": "https://epic-stream-api.netlify.app/assets/shows/62199cb2-14ad-4035-9060-3f8f93ab7f08.jpg", 538 | "genres": [1, 2, 17], 539 | "seasons": 2, 540 | "date": "1991-06-09T22:00:00.000Z" 541 | }, 542 | { 543 | "id": "ac46da29-2173-43c5-8ce5-a6aac65bf0fa", 544 | "description": "With the help of the powerful Dragonballs, a team of fighters led by the saiyan warrior Goku defend the planet earth from extraterrestrial enemies.", 545 | "title": "Dragon Ball Z", 546 | "image": "https://epic-stream-api.netlify.app/assets/shows/ac46da29-2173-43c5-8ce5-a6aac65bf0fa.jpg", 547 | "genres": [8, 9, 10], 548 | "seasons": 1, 549 | "date": "1997-05-23T22:00:00.000Z" 550 | }, 551 | { 552 | "id": "e338ef67-afe5-4689-86be-c2b563f926dc", 553 | "description": "A chronicled look at the criminal exploits of Colombian drug lord Pablo Escobar, as well as the many other drug kingpins who plagued the country through the years.", 554 | "title": "Narcos", 555 | "image": "https://epic-stream-api.netlify.app/assets/shows/e338ef67-afe5-4689-86be-c2b563f926dc.jpg", 556 | "genres": [13, 1, 2], 557 | "seasons": 3, 558 | "date": "2017-08-31T22:00:00.000Z" 559 | } 560 | ] 561 | -------------------------------------------------------------------------------- /src/data/genres.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": "All Shows", 3 | "1": "Crime", 4 | "2": "Drama", 5 | "3": "Thriller", 6 | "4": "Documentary", 7 | "5": "Nature", 8 | "6": "War", 9 | "7": "History", 10 | "8": "Animation", 11 | "9": "Action", 12 | "10": "Adventure", 13 | "11": "Science", 14 | "12": "Family", 15 | "13": "Biography", 16 | "14": "Sport", 17 | "15": "Fantasy", 18 | "16": "Horror", 19 | "17": "Mystery", 20 | "18": "Comedy", 21 | "19": "Sci-Fi", 22 | "20": "Music", 23 | "21": "News", 24 | "23": "Romance" 25 | } 26 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/modules/local.ts: -------------------------------------------------------------------------------- 1 | const LOCAL_KEY = "b61a7d95-8aea-4a5c-8827-94605f9ae1eb"; 2 | 3 | const swap = ( 4 | array: string[], 5 | [from, to]: [number, number] 6 | ) => { 7 | const newArray = [...array]; 8 | const [item] = newArray.splice(from, 1); 9 | newArray.splice(to, 0, item); 10 | return newArray; 11 | }; 12 | 13 | export const get = (): string[] => { 14 | const string = window.localStorage.getItem(LOCAL_KEY); 15 | if (!string) return []; 16 | return JSON.parse(string); 17 | }; 18 | 19 | export const set = (array: string[]) => { 20 | window.localStorage.setItem( 21 | LOCAL_KEY, 22 | JSON.stringify(array) 23 | ); 24 | }; 25 | 26 | export const has = (id: string) => { 27 | return get().includes(id); 28 | }; 29 | 30 | export const move = ( 31 | direction: "up" | "down", 32 | id: string 33 | ) => { 34 | const index = get().indexOf(id); 35 | 36 | if (direction === "up") { 37 | return set(swap(get(), [index, index - 1])); 38 | } 39 | 40 | return set(swap(get(), [index, index + 1])); 41 | }; 42 | 43 | export const remove = (id: string) => { 44 | set(get().filter((item) => item !== id)); 45 | }; 46 | 47 | export const add = (id: string) => { 48 | set([id, ...get()]); 49 | }; 50 | 51 | export const toggle = (id: string): boolean => { 52 | const newValue = !has(id); 53 | if (!newValue) remove(id); 54 | if (newValue) add(id); 55 | return newValue; 56 | }; 57 | -------------------------------------------------------------------------------- /src/pages/browse/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Shell from "../../components/shell.astro"; 3 | import Details from "../../components/details.astro"; 4 | import content from "../../data/content.json"; 5 | 6 | export const getStaticPaths = () => { 7 | return content.map((item) => ({ 8 | params: { id: item.id }, 9 | })); 10 | }; 11 | 12 | const { id } = Astro.params; 13 | const match = content.find((item) => item.id === id); 14 | 15 | if (!match) { 16 | Astro.meta({ status: 404 }); 17 | } 18 | 19 | const { title } = match as any; 20 | --- 21 | 22 | 92 | 93 | 94 |
    95 |

    100 | {title} 101 |

    102 | 103 |
    104 |
    105 | 106 | Close 107 |
    108 |
    109 | 110 | 114 | 115 |
    116 | 117 | 118 | 141 | 142 | 145 | -------------------------------------------------------------------------------- /src/pages/browse/genre-[key].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Shell from "../../components/shell.astro"; 3 | import Shows from "../../components/shows.astro"; 4 | import genresRef from "../../data/genres.json"; 5 | import Genres from "../../components/genres.astro"; 6 | 7 | export const getStaticPaths = () => { 8 | return Object.keys(genresRef).map((key) => ({ 9 | params: { key }, 10 | })); 11 | }; 12 | 13 | const { key } = Astro.params; 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/pages/browse/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Shell from "../../components/shell.astro"; 3 | import Genres from '../../components/genres.astro'; 4 | import Shows from '../../components/shows.astro'; 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/favourites.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Shell from "../components/shell.astro"; 3 | --- 4 | 5 | 82 | 83 | 84 |
    85 |
    86 | 87 | 183 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Shell from "../components/shell.astro"; 3 | import Shows from "../components/shows.astro"; 4 | --- 5 | 6 | 25 | 26 | 27 | 54 | 55 | --------------------------------------------------------------------------------