-
-
Play now
-
Playing..
+
+ {!loading && coverImage && }
+
+
);
diff --git a/src/common/playlists/playThumbnail.css b/src/common/playlists/playThumbnail.css
new file mode 100644
index 0000000000..e190b6fe68
--- /dev/null
+++ b/src/common/playlists/playThumbnail.css
@@ -0,0 +1,42 @@
+/* source for shimmer : https://codepen.io/andru255/pen/wvBdxbb */
+.loader-shimmer {
+ position: relative;
+ -webkit-box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
+ background-color: #fff;
+ border-radius: 6px;
+ height: 500px;
+ overflow: hidden;
+}
+.shimmer {
+ animation-duration: 2.2s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: shimmer;
+ animation-timing-function: linear;
+ background: #ddd;
+ background: linear-gradient(to right, #f6f6f6 8%, #f0f0f0 18%, #f6f6f6 33%);
+ background-size: 1200px 100%;
+}
+
+@-webkit-keyframes shimmer {
+ 0% {
+ background-position: -100% 0;
+ }
+ 100% {
+ background-position: 100% 0;
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -1200px 0;
+ }
+ 100% {
+ background-position: 1200px 0;
+ }
+}
+
+.media {
+ height: 200px;
+}
diff --git a/src/common/playlists/play_meta.css b/src/common/playlists/play_meta.css
new file mode 100644
index 0000000000..dade664e0a
--- /dev/null
+++ b/src/common/playlists/play_meta.css
@@ -0,0 +1,17 @@
+.meta-link {
+ display: flex;
+ justify-content: space-between;
+ padding: 20px 40px;
+ align-items: center;
+ height: 60px;
+}
+
+.meta-link a {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid #000;
+}
diff --git a/src/common/search/FilterPlays.jsx b/src/common/search/FilterPlays.jsx
index 7160550fe1..7a7286a6ba 100644
--- a/src/common/search/FilterPlays.jsx
+++ b/src/common/search/FilterPlays.jsx
@@ -152,7 +152,7 @@ const FilterPlays = ({ onChange, query }) => {
options={
field.sorted
? orderBy(loadedData[field.datafield], [field.fieldName], ['asc'])
- : loadedData[field.datafield]
+ : loadedData[field.datafield] || []
}
renderInput={(params) => (
diff --git a/src/common/utils/commonUtils.js b/src/common/utils/commonUtils.js
index 1e60586064..41a44d2f73 100644
--- a/src/common/utils/commonUtils.js
+++ b/src/common/utils/commonUtils.js
@@ -42,3 +42,13 @@ export function formatDate(data) {
return `Joined ${day} ${datemonth[1]} ${year}`;
}
+
+export function getPlayPath(all_plays, play) {
+ const fileName = all_plays.has(play?.component) ? play?.component : play?.title_name;
+
+ return `${play.slug}/${fileName}`;
+}
+
+export function getPlayLink(play) {
+ return `/plays/${encodeURI(play.github.toLowerCase())}/${play.slug}`;
+}
diff --git a/src/common/utils/coverImageUtil.js b/src/common/utils/coverImageUtil.js
index 144748e5fc..4e3536e4a6 100644
--- a/src/common/utils/coverImageUtil.js
+++ b/src/common/utils/coverImageUtil.js
@@ -1,6 +1,9 @@
-export async function loadCoverImage(playSlug) {
- const acceptedImgExtensions = [`png`, `jpg`, `jpeg`];
- const imgPromises = acceptedImgExtensions.map((ext) => import(`plays/${playSlug}/cover.${ext}`));
+export const loadCoverImage = async (upPath = '', playSlug) => {
+ const acceptedImgExtensions = [`png`, `jpg`, `jpeg`, 'webp'];
+
+ const imgPromises = acceptedImgExtensions.map((ext) =>
+ import(/* @vite-ignore */ `${upPath}plays/${playSlug}/cover.${ext}`)
+ );
const response = await Promise.allSettled(imgPromises);
@@ -8,5 +11,9 @@ export async function loadCoverImage(playSlug) {
(result) => result.status === 'fulfilled' && result.value.default
);
- return fulfilledResult?.value.default;
-}
+ if (fulfilledResult?.value.default) {
+ return fulfilledResult?.value.default;
+ }
+
+ return null;
+};
diff --git a/src/images/thumb-play.webp b/src/images/thumb-play.webp
new file mode 100644
index 0000000000..faf024d801
Binary files /dev/null and b/src/images/thumb-play.webp differ
diff --git a/src/images/thumb-play_small.png b/src/images/thumb-play_small.png
new file mode 100644
index 0000000000..15d972dff0
Binary files /dev/null and b/src/images/thumb-play_small.png differ
diff --git a/src/images/thumb-play_small.webp b/src/images/thumb-play_small.webp
new file mode 100644
index 0000000000..68405e3134
Binary files /dev/null and b/src/images/thumb-play_small.webp differ
diff --git a/src/index.jsx b/src/index.jsx
index d80e9f454e..aaa6365e88 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,10 +1,10 @@
import RouteDefs from 'common/routing/RouteDefs';
import { SearchContextProvider } from 'common/search/search-context';
-import 'index.css';
+import './index.css';
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
-import reportWebVitals from 'reportWebVitals';
-import register from './registerServiceWorker';
+import reportWebVitals from './reportWebVitals';
+import { registerSW } from 'virtual:pwa-register';
import ErrorBoundry from './ErrorBoundary/ErrorBoundary';
import Notification from 'common/components/Notification';
import 'react-toastify/dist/ReactToastify.css';
@@ -52,7 +52,13 @@ const container = document.getElementById('root');
createRoot(container).render(
);
// Makes the app to work offline and load faster
-register();
+const updateSW = registerSW({
+ onNeedRefresh() {
+ if (confirm('New content available. Reload?')) {
+ updateSW(true);
+ }
+ }
+});
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
diff --git a/src/service-worker.js b/src/service-worker.js
index fae0169328..2facd09bd8 100644
--- a/src/service-worker.js
+++ b/src/service-worker.js
@@ -1,30 +1,40 @@
/* eslint-disable no-restricted-globals */
-import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
+import {
+ cleanupOutdatedCaches,
+ createHandlerBoundToURL,
+ precacheAndRoute
+} from 'workbox-precaching';
import { clientsClaim } from 'workbox-core';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
+cleanupOutdatedCaches();
+
+self.skipWaiting();
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST);
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
-registerRoute(({ request, url }) => {
- if (request.mode !== 'navigate') {
- return false;
- }
- if (url.pathname.startsWith('/_')) {
- return false;
- }
- if (url.pathname.match(fileExtensionRegexp)) {
- return false;
- }
+registerRoute(
+ ({ request, url }) => {
+ if (request.mode !== 'navigate') {
+ return false;
+ }
+ if (url.pathname.startsWith('/_')) {
+ return false;
+ }
+ if (url.pathname.match(fileExtensionRegexp)) {
+ return false;
+ }
- return true;
-}, createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'));
+ return true;
+ },
+ createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
+);
registerRoute(
({ url }) => {
diff --git a/src/setupTests.js b/src/setupTests.js
index 8f2609b7b3..98976b1755 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -2,4 +2,5 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
+import { expect } from 'vitest';
+import '@testing-library/jest-dom/vitest';
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000000..2ca039eada
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+// /
+// /
diff --git a/tailwind.config.js b/tailwind.config.js
index 5d00e3a624..459379c9f6 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,5 +1,7 @@
+/** @type {import('tailwindcss').Config} */
+
module.exports = {
- content: ['./src/**/*.{js,jsx,ts,tsx}'],
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './plays/**/*.{js,jsx,ts,tsx}', './index.html'],
theme: {
screens: {
xs: { min: '310px' },
diff --git a/tsconfig.json b/tsconfig.json
index f770db1c85..2eef9db9e3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,17 +1,31 @@
{
"compilerOptions": {
- "jsx": "react",
- "baseUrl": "src",
- "outDir": "./dist/",
- "noImplicitAny": true,
- "module": "es6",
- "lib": ["es2022", "ES6", "DOM"],
- "target": "es6",
- "allowJs": true,
+ "target": "ESNext",
+ "baseUrl": ".",
+ "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
"moduleResolution": "node",
- "suppressImplicitAnyIndexErrors": false,
"resolveJsonModule": true,
- "allowSyntheticDefaultImports": true
+ "isolatedModules": true,
+ "noEmit": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsx": "react-jsx",
+ "types": ["vitest/globals", "vite/client", "vite-plugin-svgr/client", "vite-plugin-pwa/client"],
+ "paths": {
+ "@/*": ["./src/*"],
+ "common": ["./src/common/*"],
+ "constants": ["./src/constants/*"],
+ "ErrorBondary": ["./src/ErrorBondary/*"],
+ "images": ["./src/images/*"],
+ "meta": ["./src/meta/*"],
+ "plays": ["./plays/*"]
+ }
},
"include": ["src/**/*", "public/serviceWorker.js", "src/globals.d.ts"]
}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000000..8158efc9a0
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,123 @@
+// /
+
+import { defineConfig, loadEnv } from 'vite';
+import { splitVendorChunkPlugin } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'node:path';
+import svgr from '@svgr/rollup';
+// import svgr from 'vite-plugin-svgr'
+import url from '@rollup/plugin-url';
+import EnvironmentPlugin from 'vite-plugin-environment';
+import { createHtmlPlugin } from 'vite-plugin-html';
+import tsconfigPaths from 'vite-tsconfig-paths';
+// import react from "@vitejs/plugin-react-swc";
+import { VitePWA } from 'vite-plugin-pwa';
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command, mode }) => {
+ // Load app-level env vars to node-level env vars.
+ const localEnv = loadEnv(mode, process.cwd(), '');
+ Object.assign(process.env, localEnv);
+ const { DEV_PORT = 3000 } = process.env;
+ const isDevelopment = mode === 'development';
+
+ const htmlPlugin = () => {
+ return {
+ name: 'html-transform',
+ transformIndexHtml(html: string) {
+ return html.replace(/%(.*?)%/g, function (match, p1) {
+ return localEnv[p1];
+ });
+ }
+ };
+ };
+
+ return {
+ // define: {
+ // global: {},
+ // // "process.env.NODE_ENV": `"${mode}"`
+ // },
+ server: {
+ host: true,
+ port: DEV_PORT,
+ open: true
+ // proxy: {
+ // '/dev-api': {
+ // target: 'XXX.com',
+ // changeOrigin: true,
+ // rewrite: (path) => path.replace(/^\/dev-api/, '')
+ // }
+ // }
+ },
+
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ src: resolve(__dirname, './src'),
+ App: resolve(__dirname, './src/App.jsx'),
+ common: resolve(__dirname, './src/common'),
+ constants: resolve(__dirname, './src/constants'),
+ errorBondary: resolve(__dirname, './src/errorBondary'),
+ images: resolve(__dirname, './src/images'),
+ meta: resolve(__dirname, './src/meta'),
+ plays: resolve(__dirname, './plays')
+ },
+
+ extensions: ['.jsx', '.tsx', '.js', '.ts', '.json']
+ },
+ envPrefix: 'REACT_APP_',
+ plugins: [
+ url(),
+ svgr(),
+ tsconfigPaths(),
+ // react({ jsxImportSource: "@emotion/react" }),
+ react(),
+ htmlPlugin(),
+ EnvironmentPlugin({ REACT_APP_DEV_PORT: DEV_PORT }),
+ EnvironmentPlugin('all', { prefix: 'REACT_APP_' }),
+ VitePWA({
+ registerType: 'autoUpdate',
+ strategies: 'injectManifest',
+ srcDir: 'src',
+ filename: 'service-worker.js',
+ // add this to cache all the imports
+ workbox: {
+ globPatterns: ['**/*']
+ },
+ // add this to cache all the
+ // static assets in the public folder
+ includeAssets: ['**/*'],
+ devOptions: {
+ enabled: true
+ /* other options */
+ }
+ })
+ ],
+
+ test: {
+ globals: true,
+ environment: 'happy-dom',
+ setupFiles: './src/setupTests.js',
+ css: true,
+ reporters: ['verbose'],
+ coverage: {
+ reporter: ['text', 'json', 'html'],
+ include: [
+ `./src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}`,
+ `./plays/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}`
+ ],
+ exclude: [
+ '**/node_modules/**',
+ '**/build/**',
+ '**/dist/**',
+ '**/cypress/**',
+ '**/.{idea,git,cache,output,temp}/**',
+ '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*'
+ ]
+ }
+ },
+ build: {
+ outDir: 'build'
+ }
+ };
+});
diff --git a/yalc.lock b/yalc.lock
new file mode 100644
index 0000000000..10018a930b
--- /dev/null
+++ b/yalc.lock
@@ -0,0 +1,9 @@
+{
+ "version": "v1",
+ "packages": {
+ "create-react-play": {
+ "signature": "f9f8ee7b9fb10be45e4a8dd6fe4845b1",
+ "file": true
+ }
+ }
+}
\ No newline at end of file