diff --git a/website/src/repl/components/incrementor/Incrementor.jsx b/website/src/repl/components/incrementor/Incrementor.jsx new file mode 100644 index 00000000..5f722fb7 --- /dev/null +++ b/website/src/repl/components/incrementor/Incrementor.jsx @@ -0,0 +1,67 @@ +import { Textbox } from '../textbox/Textbox'; +import cx from '@src/cx.mjs'; + +function IncButton({ children, className, ...buttonProps }) { + return ( + + ); +} +export function Incrementor({ + onChange, + value, + min = -Infinity, + max = Infinity, + className, + incrementLabel = 'next page', + decrementLabel = 'prev page', + ...incrementorProps +}) { + value = parseInt(value); + value = isNaN(value) ? '' : value; + return ( +
rounded-md', className)}> + { + if (v.length && v < min) { + return; + } + onChange(v); + }} + type="number" + placeholder="" + value={value} + className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + {...incrementorProps} + /> +
+ onChange(value - 1)} aria-label={decrementLabel}> + + + + + = max} + onClick={() => onChange(value + 1)} + aria-label={incrementLabel} + > + + + + +
+
+ ); +} diff --git a/website/src/repl/components/pagination/Pagination.jsx b/website/src/repl/components/pagination/Pagination.jsx new file mode 100644 index 00000000..c3ea6dd4 --- /dev/null +++ b/website/src/repl/components/pagination/Pagination.jsx @@ -0,0 +1,5 @@ +import { Incrementor } from '../incrementor/Incrementor'; + +export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) { + return ; +} diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index df6075eb..21449444 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -1,6 +1,8 @@ import { exportPatterns, importPatterns, + loadAndSetFeaturedPatterns, + loadAndSetPublicPatterns, patternFilterName, useActivePattern, useViewingPatternData, @@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { parseJSON, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { settingsMap, useSettings } from '../../../settings.mjs'; - -function classNames(...classes) { - return classes.filter(Boolean).join(' '); -} +import { Pagination } from '../pagination/Pagination.jsx'; +import { useState } from 'react'; +import { useDebounce } from '../usedebounce.jsx'; +import cx from '@src/cx.mjs'; export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { const meta = useMemo(() => getMetadata(pattern.code), [pattern]); @@ -33,13 +35,15 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { if (title == null) { title = 'unnamed'; } - return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}; + + const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'; + return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}; } function PatternButton({ showOutline, onClick, pattern, showHiglight }) { return ( - {Object.values(patterns) + {Object.values(patterns ?? {}) .reverse() .map((pattern) => { const id = pattern.id; @@ -84,82 +88,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) { ); } -export function PatternsTab({ context }) { +const updateCodeWindow = (context, patternData, reset = false) => { + context.handleUpdate(patternData, reset); +}; + +const autoResetPatternOnChange = !isUdels(); + +function UserPatterns({ context }) { const activePattern = useActivePattern(); const viewingPatternStore = useViewingPatternData(); const viewingPatternData = parseJSON(viewingPatternStore); - const { userPatterns, patternFilter } = useSettings(); - - const examplePatterns = useExamplePatterns(); - const collections = examplePatterns.collections; - - const updateCodeWindow = (patternData, reset = false) => { - context.handleUpdate(patternData, reset); - }; const viewingPatternID = viewingPatternData?.id; - - const autoResetPatternOnChange = !isUdels(); - return ( -
- settingsMap.setKey('patternFilter', value)} - items={patternFilterName} - > - {patternFilter === patternFilterName.user && ( -
-
- { - const { data } = userPattern.createAndAddToDB(); - updateCodeWindow(data); - }} - /> - { - const { data } = userPattern.duplicate(viewingPatternData); - updateCodeWindow(data); - }} - /> - { - const { data } = userPattern.delete(viewingPatternID); - updateCodeWindow({ ...data, collection: userPattern.collection }); - }} - /> - - +
+
+ { + const { data } = userPattern.createAndAddToDB(); + updateCodeWindow(context, data); + }} + /> + { + const { data } = userPattern.duplicate(viewingPatternData); + updateCodeWindow(context, data); + }} + /> + { + const { data } = userPattern.delete(viewingPatternID); + updateCodeWindow(context, { ...data, collection: userPattern.collection }); + }} + /> + + - { - const { data } = userPattern.clearAll(); - updateCodeWindow(data); - }} - /> -
-
- )} + { + const { data } = userPattern.clearAll(); + updateCodeWindow(context, data); + }} + /> +
-
+
{patternFilter === patternFilterName.user && ( - updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange) + updateCodeWindow( + context, + { ...userPatterns[id], collection: userPattern.collection }, + autoResetPatternOnChange, + ) } patterns={userPatterns} started={context.started} @@ -167,24 +161,111 @@ export function PatternsTab({ context }) { viewingPatternID={viewingPatternID} /> )} - {patternFilter !== patternFilterName.user && - Array.from(collections.keys()).map((collection) => { - const patterns = collections.get(collection); - return ( -
-

{collection}

-
- updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)} - started={context.started} - patterns={patterns} - activePattern={activePattern} - /> -
-
- ); - })} -
+
+
+ ); +} + +function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) { + const [page, setPage] = useState(initialPage); + const debouncedPageChange = useDebounce(() => { + paginationOnChange(page); + }); + + const onPageChange = (pageNum) => { + setPage(pageNum); + debouncedPageChange(); + }; + + const activePattern = useActivePattern(); + return ( +
+
+ patternOnClick(id)} + started={context.started} + patterns={patterns} + activePattern={activePattern} + /> +
+
+ + +
+
+ ); +} + +let featuredPageNum = 1; +function FeaturedPatterns({ context }) { + const examplePatterns = useExamplePatterns(); + const collections = examplePatterns.collections; + const patterns = collections.get(patternFilterName.featured); + return ( + { + updateCodeWindow( + context, + { ...patterns[id], collection: patternFilterName.featured }, + autoResetPatternOnChange, + ); + }} + paginationOnChange={async (pageNum) => { + await loadAndSetFeaturedPatterns(pageNum - 1); + featuredPageNum = pageNum; + }} + /> + ); +} + +let latestPageNum = 1; +function LatestPatterns({ context }) { + const examplePatterns = useExamplePatterns(); + const collections = examplePatterns.collections; + const patterns = collections.get(patternFilterName.public); + return ( + { + updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange); + }} + paginationOnChange={async (pageNum) => { + await loadAndSetPublicPatterns(pageNum - 1); + latestPageNum = pageNum; + }} + /> + ); +} + +function PublicPatterns({ context }) { + const { patternFilter } = useSettings(); + if (patternFilter === patternFilterName.featured) { + return ; + } + return ; +} + +export function PatternsTab({ context }) { + const { patternFilter } = useSettings(); + + return ( +
+ settingsMap.setKey('patternFilter', value)} + items={patternFilterName} + > + + {patternFilter === patternFilterName.user ? ( + + ) : ( + + )}
); } diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index fbbf0a08..b2bdd543 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; import jsdocJson from '../../../../../doc.json'; +import { Textbox } from '../textbox/Textbox'; const availableFunctions = jsdocJson.docs .filter(({ name, description }) => name && !name.startsWith('_') && !!description) .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); @@ -28,12 +29,7 @@ export function Reference() {
- setSearch(event.target.value)} - /> +
{visibleFunctions.map((entry, i) => ( diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index b968466f..8ca2d39d 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react'; import { settingsMap, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; +import { Textbox } from '../textbox/Textbox.jsx'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; @@ -53,12 +54,7 @@ export function SoundsTab() { return (
- setSearch(e.target.value)} - /> + setSearch(v)} />
onChange(e.target.value)} + {...inputProps} + /> + ); +} diff --git a/website/src/repl/components/usedebounce.jsx b/website/src/repl/components/usedebounce.jsx new file mode 100644 index 00000000..a0fb1ea7 --- /dev/null +++ b/website/src/repl/components/usedebounce.jsx @@ -0,0 +1,30 @@ +import { useMemo } from 'react'; +import { useEffect } from 'react'; +import { useRef } from 'react'; + +function debounce(fn, wait) { + let timer; + return function (...args) { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => fn(...args), wait); + }; +} + +export function useDebounce(callback) { + const ref = useRef; + useEffect(() => { + ref.current = callback; + }, [callback]); + + const debouncedCallback = useMemo(() => { + const func = () => { + ref.current?.(); + }; + + return debounce(func, 1000); + }, []); + + return debouncedCallback; +} diff --git a/website/src/repl/useExamplePatterns.jsx b/website/src/repl/useExamplePatterns.jsx index 08629d44..5c4277bb 100644 --- a/website/src/repl/useExamplePatterns.jsx +++ b/website/src/repl/useExamplePatterns.jsx @@ -1,4 +1,4 @@ -import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs'; +import { $featuredPatterns, $publicPatterns, patternFilterName } from '../user_pattern_utils.mjs'; import { useStore } from '@nanostores/react'; import { useMemo } from 'react'; import * as tunes from '../repl/tunes.mjs'; @@ -12,9 +12,9 @@ export const useExamplePatterns = () => { const publicPatterns = useStore($publicPatterns); const collections = useMemo(() => { const pats = new Map(); - pats.set(collectionName.featured, featuredPatterns); - pats.set(collectionName.public, publicPatterns); - // pats.set(collectionName.stock, stockPatterns); + pats.set(patternFilterName.featured, featuredPatterns); + pats.set(patternFilterName.public, publicPatterns); + // pats.set(patternFilterName.stock, stockPatterns); return pats; }, [featuredPatterns, publicPatterns]); diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 866491d0..5622d563 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -8,16 +8,12 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs'; export let $publicPatterns = atom([]); export let $featuredPatterns = atom([]); -export const collectionName = { - user: 'user', - public: 'Last Creations', - stock: 'Stock Examples', - featured: 'Featured', -}; - +const patternQueryLimit = 20; export const patternFilterName = { - community: 'community', + public: 'latest', + featured: 'featured', user: 'user', + // stock: 'stock examples', }; const sessionAtom = (name, initial = undefined) => { @@ -36,7 +32,7 @@ const sessionAtom = (name, initial = undefined) => { export let $viewingPatternData = sessionAtom('viewingPatternData', { id: '', code: '', - collection: collectionName.user, + collection: patternFilterName.user, created_at: Date.now(), }); @@ -51,25 +47,50 @@ export const setViewingPatternData = (data) => { $viewingPatternData.set(JSON.stringify(data)); }; -export function loadPublicPatterns() { - return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false }); +function parsePageNum(page) { + return isNaN(page) ? 0 : page; +} +export function loadPublicPatterns(page) { + page = parsePageNum(page); + const offset = page * patternQueryLimit; + return supabase + .from('code_v1') + .select() + .eq('public', true) + .range(offset, offset + patternQueryLimit) + .order('id', { ascending: false }); } -export function loadFeaturedPatterns() { - return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false }); +export function loadFeaturedPatterns(page = 0) { + page = parsePageNum(page); + const offset = page * patternQueryLimit; + return supabase + .from('code_v1') + .select() + .eq('featured', true) + .range(offset, offset + patternQueryLimit) + .order('id', { ascending: false }); +} + +export async function loadAndSetPublicPatterns(page) { + const p = await loadPublicPatterns(page); + const data = p?.data; + const pats = {}; + data?.forEach((data, key) => (pats[data.id ?? key] = data)); + $publicPatterns.set(pats); +} +export async function loadAndSetFeaturedPatterns(page) { + const p = await loadFeaturedPatterns(page); + const data = p?.data; + const pats = {}; + data?.forEach((data, key) => (pats[data.id ?? key] = data)); + $featuredPatterns.set(pats); } export async function loadDBPatterns() { try { - const { data: publicPatterns } = await loadPublicPatterns(); - const { data: featuredPatterns } = await loadFeaturedPatterns(); - const featured = {}; - const pub = {}; - - publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data)); - featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data)); - $publicPatterns.set(pub); - $featuredPatterns.set(featured); + await loadAndSetPublicPatterns(); + await loadAndSetFeaturedPatterns(); } catch (err) { console.error('error loading patterns', err); } @@ -92,7 +113,7 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code); const defaultCode = ''; export const userPattern = { - collection: collectionName.user, + collection: patternFilterName.user, getAll() { const patterns = parseJSON(settingsMap.get().userPatterns); return patterns ?? {};