From 50f076084f56be3bc1cfaf20c7174200d3ace34d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 18 Feb 2025 01:48:17 -0500 Subject: [PATCH] working --- .../components/incrementor/Incrementor.jsx | 52 +++++++++ .../repl/components/pagination/Pagination.jsx | 7 ++ .../src/repl/components/panel/PatternsTab.jsx | 109 ++++++++++++++---- .../src/repl/components/panel/Reference.jsx | 8 +- .../src/repl/components/panel/SoundsTab.jsx | 6 +- .../src/repl/components/textbox/Textbox.jsx | 15 +++ website/src/repl/components/usedebounce.jsx | 30 +++++ website/src/repl/util.mjs | 1 + website/src/settings.mjs | 2 + website/src/user_pattern_utils.mjs | 44 ++++--- website/tailwind.config.cjs | 2 + website/tailwind_utils.mjs | 4 + 12 files changed, 237 insertions(+), 43 deletions(-) create mode 100644 website/src/repl/components/incrementor/Incrementor.jsx create mode 100644 website/src/repl/components/pagination/Pagination.jsx create mode 100644 website/src/repl/components/textbox/Textbox.jsx create mode 100644 website/src/repl/components/usedebounce.jsx create mode 100644 website/tailwind_utils.mjs diff --git a/website/src/repl/components/incrementor/Incrementor.jsx b/website/src/repl/components/incrementor/Incrementor.jsx new file mode 100644 index 00000000..1bad0a8d --- /dev/null +++ b/website/src/repl/components/incrementor/Incrementor.jsx @@ -0,0 +1,52 @@ +import { cn } from 'tailwind_utils.mjs'; +import { Textbox } from '../textbox/Textbox'; + +function IncButton({ children, label, className, ...buttonProps }) { + return ( + + ); +} +export function Incrementor({ onChange, value, min = -Infinity, max = Infinity, className }) { + 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 my-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ onChange(value - 1)}> + + + + + = max} onClick={() => onChange(value + 1)}> + + + + +
+
+ ); +} diff --git a/website/src/repl/components/pagination/Pagination.jsx b/website/src/repl/components/pagination/Pagination.jsx new file mode 100644 index 00000000..e9953136 --- /dev/null +++ b/website/src/repl/components/pagination/Pagination.jsx @@ -0,0 +1,7 @@ + +import { Incrementor } from '../incrementor/Incrementor'; + +export function Pagination({ currPage, onPageChange, className }) { + return ; +} + diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index fd9ddb15..e809d654 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,6 +14,9 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { parseJSON, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { settingsMap, useSettings } from '../../../settings.mjs'; +import { Pagination } from '../pagination/Pagination.jsx'; +import { useState } from 'react'; +import { useDebounce } from '../usedebounce.jsx'; function classNames(...classes) { return classes.filter(Boolean).join(' '); @@ -33,7 +38,9 @@ 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 }) { @@ -57,7 +64,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) { const viewingPatternID = viewingPatternData.id; return (
- {Object.values(patterns) + {Object.values(patterns ?? {}) .reverse() .map((pattern) => { const id = pattern.id; @@ -97,8 +104,8 @@ function UserPatterns({ context }) { const { userPatterns, patternFilter } = useSettings(); const viewingPatternID = viewingPatternData?.id; return ( -
-
+
+
{ @@ -141,7 +148,7 @@ function UserPatterns({ context }) { />
-
+
{patternFilter === patternFilterName.user && ( @@ -162,31 +169,93 @@ function UserPatterns({ context }) { ); } -function PublicPatterns({ context }) { +function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange }) { + const [page, setPage] = useState(1); + const debouncedPageChange = useDebounce(() => { + paginationOnChange(page); + }); + + const onPageChange = (pageNum) => { + setPage(pageNum); + debouncedPageChange(); + }; + const activePattern = useActivePattern(); - const examplePatterns = useExamplePatterns(); - const collections = examplePatterns.collections; - const { patternFilter } = useSettings(); - const patterns = collections.get(patternFilter) ?? collections.get(patternFilterName.public); return ( -
- - updateCodeWindow(context, { ...patterns[id], collection: patternFilter }, autoResetPatternOnChange) - } - started={context.started} - patterns={patterns} - activePattern={activePattern} - /> +
+
+ patternOnClick(id)} + started={context.started} + patterns={patterns} + activePattern={activePattern} + /> +
+
+ {' '} + +
); } +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); + }} + /> + ); +} + +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); + }} + /> + ); +} + +function PublicPatterns({ context }) { + const { patternFilter } = useSettings(); + if (patternFilter === patternFilterName.featured) { + return ; + } + return ; +} + export function PatternsTab({ context }) { const { patternFilter } = useSettings(); return ( -
+
settingsMap.setKey('patternFilter', value)} diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index fbbf0a08..a8b8a301 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..3d5e2d2c 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,11 +54,10 @@ export function SoundsTab() { return (
- setSearch(e.target.value)} + onChange={(v) => setSearch(v)} />
diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx new file mode 100644 index 00000000..da99b17b --- /dev/null +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -0,0 +1,15 @@ +import { cn } from 'tailwind_utils.mjs'; +// type TextboxProps = { +// onChange: (val: string | number) => void; +// ...inputProps +// } +export function Textbox(props) { + const {onChange, className, ...inputProps} = props + return ( + props.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/util.mjs b/website/src/repl/util.mjs index f623e468..ddd91453 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -199,3 +199,4 @@ export function setVersionDefaultsFrom(code) { console.error(err); } } + diff --git a/website/src/settings.mjs b/website/src/settings.mjs index cd3d8936..8c35e996 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -40,6 +40,8 @@ export const defaultSettings = { audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, + publicPatternPage: 1, + featuredPatternPage: 1, }; let search = null; diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index d87fbff4..a1a344db 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -9,7 +9,7 @@ export let $publicPatterns = atom([]); export let $featuredPatterns = atom([]); - +const patternQueryLimit = 20 export const patternFilterName = { public: 'latest', featured: 'featured', @@ -48,25 +48,41 @@ export const setViewingPatternData = (data) => { $viewingPatternData.set(JSON.stringify(data)); }; -export function loadPublicPatterns() { - return supabase.from('code_v1').select().eq('public', true).limit(40).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(40).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); } diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index 23ae96c3..83793161 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -2,6 +2,8 @@ const defaultTheme = require('tailwindcss/defaultTheme'); + + module.exports = { darkMode: 'class', content: [ diff --git a/website/tailwind_utils.mjs b/website/tailwind_utils.mjs new file mode 100644 index 00000000..21ed33d7 --- /dev/null +++ b/website/tailwind_utils.mjs @@ -0,0 +1,4 @@ +// utility for combining class names +export function cn(...classNameStrings) { + return classNameStrings.join(' ') + } \ No newline at end of file