matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

commit 3e81f3bd0fce4a70fd9f6958b664c75f906d7361
parent b52cb312b52ac8d631f2a628fb3105d378bc6019
Author: MTRNord <mtrnord1@gmail.com>
Date:   Tue,  3 Jan 2023 21:16:35 +0100

load images async, preconnect to matrix server and fix deploying the page

Diffstat:
M.github/workflows/deploy.yml | 3+++
Mindex.html | 1+
Mpackage-lock.json | 164++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackage.json | 6++++--
Msrc/components/header.tsx | 54+++++++++++++++++++++++-------------------------------
Msrc/components/post.tsx | 20+++++++++-----------
Msrc/pages/Home.tsx | 57+++++++++++++++++++++++++++------------------------------
Asrc/utils/asyncImages.tsx | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/utils/resources.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mvite.config.ts | 10+++++++++-
10 files changed, 337 insertions(+), 77 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml @@ -26,6 +26,9 @@ jobs: - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@v4.4.1 + env: + BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: branch: gh-pages # The branch the action should deploy to. folder: dist # The folder the action should deploy. diff --git a/index.html b/index.html @@ -11,6 +11,7 @@ global = globalThis // fix for "global not defined" error globalThis.OLM_OPTIONS = undefined; // Fix missing var for olm </script> + <link rel="preconnect" href="<%= env.VITE_MATRIX_SERVER_URL %>"> </head> <body> diff --git a/package-lock.json b/package-lock.json @@ -17,7 +17,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-plock": "github:MTRNord/react-plock#MTRNord/patch-react", - "react-router-dom": "^6.6.1" + "react-router-dom": "^6.6.1", + "react-spinners": "^0.13.7" }, "devDependencies": { "@types/events": "^3.0.0", @@ -33,7 +34,8 @@ "postcss": "^8.4.20", "tailwindcss": "^3.2.4", "typescript": "^4.9.4", - "vite": "^4.0.4" + "vite": "^4.0.4", + "vite-plugin-ejs": "^1.6.4" } }, "node_modules/@babel/code-frame": { @@ -1447,6 +1449,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.13", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", @@ -2025,6 +2033,21 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -2468,6 +2491,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2950,6 +3003,24 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-sdsl": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", @@ -3856,6 +3927,15 @@ "react-dom": ">=16.8" } }, + "node_modules/react-spinners": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.7.tgz", + "integrity": "sha512-mRamn56bfxWbGcacif5RT3UbeJaXi2AttjtPwSmomuv2IcxjpbfETCzdTvaQpNDk0E33ENJsStsQeKAZFuJcpA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4742,6 +4822,15 @@ } } }, + "node_modules/vite-plugin-ejs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vite-plugin-ejs/-/vite-plugin-ejs-1.6.4.tgz", + "integrity": "sha512-23p1RS4PiA0veXY5/gHZ60pl3pPvd8NEqdBsDgxNK8nM1rjFFDcVb0paNmuipzCgNP/Y0f/Id22M7Il4kvZ2jA==", + "dev": true, + "dependencies": { + "ejs": "^3.1.8" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -5732,6 +5821,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, "autoprefixer": { "version": "10.4.13", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", @@ -6112,6 +6207,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -6455,6 +6559,35 @@ "flat-cache": "^3.0.4" } }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6790,6 +6923,18 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, "js-sdsl": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", @@ -7409,6 +7554,12 @@ "react-router": "6.6.1" } }, + "react-spinners": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.7.tgz", + "integrity": "sha512-mRamn56bfxWbGcacif5RT3UbeJaXi2AttjtPwSmomuv2IcxjpbfETCzdTvaQpNDk0E33ENJsStsQeKAZFuJcpA==", + "requires": {} + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8012,6 +8163,15 @@ "rollup": "^3.7.0" } }, + "vite-plugin-ejs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vite-plugin-ejs/-/vite-plugin-ejs-1.6.4.tgz", + "integrity": "sha512-23p1RS4PiA0veXY5/gHZ60pl3pPvd8NEqdBsDgxNK8nM1rjFFDcVb0paNmuipzCgNP/Y0f/Id22M7Il4kvZ2jA==", + "dev": true, + "requires": { + "ejs": "^3.1.8" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json @@ -19,7 +19,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-plock": "github:MTRNord/react-plock#MTRNord/patch-react", - "react-router-dom": "^6.6.1" + "react-router-dom": "^6.6.1", + "react-spinners": "^0.13.7" }, "devDependencies": { "@types/events": "^3.0.0", @@ -35,6 +36,7 @@ "postcss": "^8.4.20", "tailwindcss": "^3.2.4", "typescript": "^4.9.4", - "vite": "^4.0.4" + "vite": "^4.0.4", + "vite-plugin-ejs": "^1.6.4" } } diff --git a/src/components/header.tsx b/src/components/header.tsx @@ -1,37 +1,29 @@ import { Link } from "react-router-dom"; -import { PureComponent } from "react"; +import { useContext } from "react"; import { Client } from "../context"; -import { MatrixClient } from "../matrix/client"; -export class Header extends PureComponent { - static contextType = Client; - declare context: React.ContextType<typeof Client>; - - render() { - const client: MatrixClient | undefined = this.context; - return ( - <div className="m-12 flex flex-col lg:flex-row items-center justify-between"> - <Link to="/"><img alt="Matrix Art" src="Logo_colored.svg" className="ease-in-out hover:scale-105 trandform transition-transform duration-300" /></Link> - <div className="flex items-center mt-8 lg:mt-0 flex-col sm:flex-row"> - <div className="flex items-center justify-between w-80 max-w-80 mx-6 ease-in-out hover:scale-105 trandform transition-transform duration-300"> - <div className="absolute ml-4"> - <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#AAB3CF"> - <path d="M0 0h24v24H0V0z" fill="none" /> - <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" /> - </svg> - </div> - <input className="search-bg shadow rounded-2xl border-0 py-3 px-4 pl-10 text-data" placeholder="Search"></input> +export function Header() { + const client = useContext(Client); + return ( + <div className="m-12 flex flex-col lg:flex-row items-center justify-between"> + <Link to="/"><img alt="Matrix Art" src="Logo_colored.svg" className="ease-in-out hover:scale-105 trandform transition-transform duration-300" /></Link> + <div className="flex items-center mt-8 lg:mt-0 flex-col sm:flex-row"> + <div className="flex items-center justify-between w-80 max-w-80 mx-6 ease-in-out hover:scale-105 trandform transition-transform duration-300"> + <div className="absolute ml-4"> + <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#AAB3CF"> + <path d="M0 0h24v24H0V0z" fill="none" /> + <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" /> + </svg> </div> - { + <input className="search-bg shadow rounded-2xl border-0 py-3 px-4 pl-10 text-data" placeholder="Search"></input> + </div> + { - !client?.isLoggedIn() ? <Link to="/join" className="text-white font-bold text-1xl logo-bg rounded-2xl py-3 px-12 shadow mt-4 sm:mt-0 transform transition-transform ease-in-out duration-300 hover:scale-105">Join</Link> : - <Link to="/" className="text-white font-bold text-1xl logo-bg rounded-2xl py-3 px-12 shadow mt-4 sm:mt-0 transform transition-transform ease-in-out duration-300 hover:scale-105">Post</Link> - } + !client?.isLoggedIn() ? <Link to="/join" className="text-white font-bold text-1xl logo-bg rounded-2xl py-3 px-12 shadow mt-4 sm:mt-0 transform transition-transform ease-in-out duration-300 hover:scale-105">Join</Link> : + <Link to="/" className="text-white font-bold text-1xl logo-bg rounded-2xl py-3 px-12 shadow mt-4 sm:mt-0 transform transition-transform ease-in-out duration-300 hover:scale-105">Post</Link> + } - </div> </div> - ); - } -} - -Header.contextType = Client; -\ No newline at end of file + </div> + ); +} +\ No newline at end of file diff --git a/src/components/post.tsx b/src/components/post.tsx @@ -1,22 +1,20 @@ -import { PureComponent } from "react"; +import { Suspense } from "react"; import { PostData } from "../data/post"; import { UserData } from "../data/user"; +import { SuspenseImage } from "../utils/asyncImages"; +import { BounceLoader } from "react-spinners"; type Props = { user: UserData; post: PostData; }; -export class Post extends PureComponent<Props> { - constructor(props: Props) { - super(props); - } - render() { - const { user, post }: Props = this.props; - return ( +export function Post({ user, post }: Props) { + return ( + <Suspense fallback={<div className="flex flex-col w-full"><BounceLoader color="#FEA500" /></div>}> <div className="flex flex-col"> <a id="gallery_click" href={post.content.file.url} className="w-full" target="_blank" rel="noreferrer"> - <img decoding="async" loading="lazy" className="rounded-3xl shadow object-cover transform transition-transform ease-in-out duration-300 hover:scale-105" src={post.content.file.url} /> + <SuspenseImage className="rounded-3xl shadow object-cover transform transition-transform ease-in-out duration-300 hover:scale-105" src={post.content.file.url} /> </a> <div className="flex items-center justify-between py-4"> <a className="flex items-center" href="#"> @@ -45,6 +43,6 @@ export class Post extends PureComponent<Props> { </div> </div> </div> - ); - } + </Suspense> + ); } \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx @@ -1,4 +1,3 @@ -import { PureComponent } from "react"; import { Header } from "../components/header"; import { Post } from "../components/post"; import { Plock } from "react-plock"; @@ -13,39 +12,37 @@ const BREAKPOINTS = [ { size: 1401, columns: 4 }, ]; -export class Home extends PureComponent { - render() { - return ( - <div className="flex flex-col"> - <header> - <Header /> - </header> - <main className="m-12 mt-6"> - <h1 className="text-3xl font-bold mb-4 text-white">Explore</h1> - <div className="flex justify-center" id="gallery"> - <Plock gap={"24px"} breakpoints={BREAKPOINTS}> - {/* +export function Home() { + return ( + <div className="flex flex-col"> + <header> + <Header /> + </header> + <main className="m-12 mt-6"> + <h1 className="text-3xl font-bold mb-4 text-white">Explore</h1> + <div className="flex justify-center" id="gallery"> + <Plock gap={"24px"} breakpoints={BREAKPOINTS}> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person A", "https://unsplash.com/photos/_cvwXhGqG-o/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8OHx8ZmFjZXxlbnwwfDJ8fHwxNjQ4Nzc5NzYx")} post={new PostData("", { file: { url: "https://unsplash.com/photos/UWmrTAQ75iA/download?ixid=MnwxMjA3fDB8MXxhbGx8Mnx8fHx8fDJ8fDE2NDg4MzI0Njg" } })} /> - {/* + <Post user={new UserData("", "Person A", "https://unsplash.com/photos/_cvwXhGqG-o/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8OHx8ZmFjZXxlbnwwfDJ8fHwxNjQ4Nzc5NzYx")} post={new PostData("", { file: { url: "https://unsplash.com/photos/UWmrTAQ75iA/download?ixid=MnwxMjA3fDB8MXxhbGx8Mnx8fHx8fDJ8fDE2NDg4MzI0Njg" } })} /> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person B", "https://unsplash.com/photos/uJ8LNVCBjFQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGZhY2V8ZW58MHwyfHx8MTY0ODc3OTc2MQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/7ptbiEPxKMQ/download?ixid=MnwxMjA3fDB8MXxhbGx8OXx8fHx8fDJ8fDE2NDg4MzI0Njg" } })} /> - {/* + <Post user={new UserData("", "Person B", "https://unsplash.com/photos/uJ8LNVCBjFQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGZhY2V8ZW58MHwyfHx8MTY0ODc3OTc2MQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/7ptbiEPxKMQ/download?ixid=MnwxMjA3fDB8MXxhbGx8OXx8fHx8fDJ8fDE2NDg4MzI0Njg" } })} /> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person C", "https://unsplash.com/photos/u3pi6HhSYew/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTd8fGZhY2V8ZW58MHwyfHx8MTY0ODc3OTc2MQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/ZiPlVmmKEYA/download?ixid=MnwxMjA3fDB8MXxhbGx8Mzl8fHx8fHwyfHwxNjQ4ODM2MDIz" } })} /> - {/* + <Post user={new UserData("", "Person C", "https://unsplash.com/photos/u3pi6HhSYew/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTd8fGZhY2V8ZW58MHwyfHx8MTY0ODc3OTc2MQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/ZiPlVmmKEYA/download?ixid=MnwxMjA3fDB8MXxhbGx8Mzl8fHx8fHwyfHwxNjQ4ODM2MDIz" } })} /> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person D", "https://unsplash.com/photos/TW_z_iUD_bQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8ODR8fHBvcnRyYWl0fGVufDB8Mnx8fDE2NDg4NDQ3MjQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/FTQJBGDxKXg/download?ixid=MnwxMjA3fDB8MXxhbGx8M3x8fHx8fDJ8fDE2NDg4NDMyNDY" } })} /> - {/* + <Post user={new UserData("", "Person D", "https://unsplash.com/photos/TW_z_iUD_bQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8ODR8fHBvcnRyYWl0fGVufDB8Mnx8fDE2NDg4NDQ3MjQ")} post={new PostData("", { file: { url: "https://unsplash.com/photos/FTQJBGDxKXg/download?ixid=MnwxMjA3fDB8MXxhbGx8M3x8fHx8fDJ8fDE2NDg4NDMyNDY" } })} /> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person E", "https://unsplash.com/photos/kYtN89E5ce4/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTA0fHxwb3J0cmFpdHxlbnwwfDJ8fHwxNjQ4ODQ0NzQy")} post={new PostData("", { file: { url: "https://unsplash.com/photos/zfhxiaZO3JE/download?ixid=MnwxMjA3fDB8MXxhbGx8Mjl8fHx8fHwyfHwxNjQ4ODQzMjQ1" } })} /> - {/* + <Post user={new UserData("", "Person E", "https://unsplash.com/photos/kYtN89E5ce4/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTA0fHxwb3J0cmFpdHxlbnwwfDJ8fHwxNjQ4ODQ0NzQy")} post={new PostData("", { file: { url: "https://unsplash.com/photos/zfhxiaZO3JE/download?ixid=MnwxMjA3fDB8MXxhbGx8Mjl8fHx8fHwyfHwxNjQ4ODQzMjQ1" } })} /> + {/* // @ts-ignore */} - <Post user={new UserData("", "Person E", "https://unsplash.com/photos/kBonaevi3yA/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTE5fHxwb3J0cmFpdHxlbnwwfDJ8fHwxNjQ4ODQ0NzQy")} post={new PostData("", { file: { url: "https://unsplash.com/photos/jm-JKqrAXsA/download?ixid=MnwxMjA3fDB8MXxhbGx8MTAyfHx8fHx8Mnx8MTY0ODg0MzI1NQ" } })} /> - </Plock> - </div> - </main> - </div> - ); - } + <Post user={new UserData("", "Person E", "https://unsplash.com/photos/kBonaevi3yA/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTE5fHxwb3J0cmFpdHxlbnwwfDJ8fHwxNjQ4ODQ0NzQy")} post={new PostData("", { file: { url: "https://unsplash.com/photos/jm-JKqrAXsA/download?ixid=MnwxMjA3fDB8MXxhbGx8MTAyfHx8fHx8Mnx8MTY0ODg0MzI1NQ" } })} /> + </Plock> + </div> + </main> + </div> + ); } \ No newline at end of file diff --git a/src/utils/asyncImages.tsx b/src/utils/asyncImages.tsx @@ -0,0 +1,45 @@ +// First we need a type of cache to avoid creating resources for images + +import React from "react"; +import { ImgHTMLAttributes } from "react"; +import { createResource, Resource } from "./resources"; + +// we have already fetched in the past +export const cache = new Map<string, any>(); + +// then we create our loadImage function, this function receives the source +// of the image and returns a resource +export function loadImage(source: string): Resource<string> { + // here we start getting the resource from the cache + let resource = cache.get(source); + // and if it's there we return it immediately + if (resource) return resource; + // but if it's not we create a new resource + resource = createResource<string>( + () => + // in our async function we create a promise + new Promise((resolve, reject) => { + // then create a new image element + const img = new window.Image(); + // set the src to our source + img.src = source; + // and start listening for the load event to resolve the promise + img.addEventListener("load", () => resolve(source)); + // and also the error event to reject the promise + img.addEventListener("error", () => + reject(new Error(`Failed to load image ${source}`)) + ); + }) + ); + // before finishing we save the new resource in the cache + cache.set(source, resource); + // and return return it + return resource; +} + +export function SuspenseImage( + props: ImgHTMLAttributes<HTMLImageElement> +): JSX.Element { + loadImage(props.src!).read(); + return <img {...props} />; +} +\ No newline at end of file diff --git a/src/utils/resources.ts b/src/utils/resources.ts @@ -0,0 +1,52 @@ +// A Resource is an object with a read method returning the payload +export interface Resource<Payload> { + read: () => Payload; +} + +export type status = "pending" | "success" | "error"; + +// this function let us get a new function using the asyncFn we pass +// this function also receives a payload and return us a resource with +// that payload assigned as type +export function createResource<Payload>( + asyncFn: () => Promise<Payload> +): Resource<Payload> { + // we start defining our resource is on a pending status + let status: status = "pending"; + // and we create a variable to store the result + let result: any; + // then we immediately start running the `asyncFn` function + // and we store the resulting promise + const promise = asyncFn().then( + (r: Payload) => { + // once it's fulfilled we change the status to success + // and we save the returned value as result + status = "success"; + result = r; + }, + (e: Error) => { + // once it's rejected we change the status to error + // and we save the returned error as result + status = "error"; + result = e; + } + ); + // lately we return an error object with the read method + return { + read(): Payload { + // here we will check the status value + switch (status) { + case "pending": + // if it's still pending we throw the promise + // throwing a promise is how Suspense know our component is not ready + throw promise; + case "error": + // if it's error we throw the error + throw result; + case "success": + // if it's success we return the result + return result; + } + }, + }; +} +\ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts @@ -1,9 +1,17 @@ import { defineConfig, splitVendorChunkPlugin } from 'vite'; import react from "@vitejs/plugin-react-swc"; +import { ViteEjsPlugin } from "vite-plugin-ejs"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), splitVendorChunkPlugin()], + plugins: [ + react(), + splitVendorChunkPlugin(), + ViteEjsPlugin((viteConfig) => ({ + // viteConfig is the current Vite resolved config + env: viteConfig.env, + })), + ], resolve: { browserField: false, },