Skip to content

Commit

Permalink
(wip) Playlist (#1025)
Browse files Browse the repository at this point in the history
* Limit playlist length and check if level is already in list

* add remove from playlist functionality

* updateOne

* cleanup

* populate flag

* tooltip

* play later rename

* handing off to sspenst

* useSWR instead of fetch for the getReviews and getRecords

* unrelated fix for formatted user level undefinede

* play later api

* fix from renames

* private and reserve play-later slug

* Private updates

* show hide list

* formatting

* left scrollbar

* auto smooth scroll to current level

* Fix title of collection

* play later link broken fix

* fix hide show

* copy tweaks

* fix build

* add test

* collections can now be marked private

* allowing creating of private levels on create

* update collection tests for private check

* cleanup

* more tests and cleanup

* 100% coverage for play-later

* optimize play-attempt.test and format it. combine promise.alls.

* test for reserved slug name

* update tests some more

* more tests

* private to isPrivate

* fix test

* improve coverage of naturalsort

* Handle PlayLater type in collection update. Allow making it private/public

* [unrelated to PR] add test for api/user/[id]/index

* gate play later behind pro

* fix tests

* add to play later from search

* cleanup

* pass stats in to selectoption

* See play collection directly from search results

* use session storage

* cleanup

* doh

* search query in title link

* fix play later link

* include more in level_search_default_projection

* add data only in search rather than generically

* 3 dot

* cleanup

* edit level modal when no collection show a bit more info about what a collection is

* move <Solved/> to top right

* move play later to top

* bring to top

* helper file

* toasterror undo

* mobile view for list of collections

* comment

* add test. cleanup. also settimeout on the scroll only for mobile

* select card ux updates

* ux updates

* fix bringToTop

* models/constants

* working on collection UX

* naturalSort custom compare function

* refactor level title out of gameLayout

* tweaking

* more efficient collection fetching

* tempCollection tweaks

* Add one click undo for play later

* fix auto scroll to level in modal

* userAgent now helps set defaults for useDeviceCheck

* prevent relayout when collection is still loading

* show + and - buttons hover effect only for desktop

* fix tests

* fixes

* more fixes

* we are getting close

* fix apis and tests

* lint

---------

Co-authored-by: Spencer Spenst <[email protected]>
  • Loading branch information
k2xl and sspenst authored Nov 2, 2023
1 parent 56faa18 commit 524b867
Show file tree
Hide file tree
Showing 86 changed files with 2,045 additions and 655 deletions.
2 changes: 1 addition & 1 deletion components/buttons/filterButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useContext } from 'react';
import { toast } from 'react-hot-toast';

interface FilterButtonProps {
element: JSX.Element | string;
element: React.ReactNode;
first?: boolean;
last?: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
Expand Down
2 changes: 1 addition & 1 deletion components/cards/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import StyledTooltip from '../page/styledTooltip';
interface CardProps {
children: React.ReactNode;
id: string;
title: JSX.Element | string;
title: React.ReactNode;
tooltip?: string;
}

Expand Down
103 changes: 103 additions & 0 deletions components/cards/playLaterToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { AppContext } from '@root/contexts/appContext';
import isPro from '@root/helpers/isPro';
import { EnrichedLevel } from '@root/models/db/level';
import Link from 'next/link';
import React, { useContext, useState } from 'react';
import toast from 'react-hot-toast';
import StyledTooltip from '../page/styledTooltip';

interface PlayLaterToggleButtonProps {
className?: string;
level: EnrichedLevel;
}

export function PlayLaterToggleButton({ className, level }: PlayLaterToggleButtonProps) {
const { mutatePlayLater, playLater, user } = useContext(AppContext);
const isInPlayLater = !!(playLater && playLater[level._id.toString()]);
const [isLoading, setIsLoading] = useState(false);

if (!user || !isPro(user) || !playLater) {
return null;
}

const boldedLevelName = <span className='font-bold'>{level.name}</span>;
const fetchFunc = async (remove: boolean) => {
setIsLoading(true);
toast.dismiss();
toast.loading(remove ? 'Removing...' : 'Adding...', {
position: 'bottom-center',
});

const res = await fetch('/api/play-later/', {
method: remove ? 'DELETE' : 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: level._id.toString(),
}),
});

toast.dismiss();

if (res.ok) {
const message = (
<div className='flex flex-col items-center w-92 max-w-full text-center'>
<span>{remove ? ['Removed ', boldedLevelName, ' from'] : ['Added ', boldedLevelName, ' to']} <Link className='underline' href={`/collection/${user.name}/play-later`}>Play Later</Link></span>
<button className='text-sm underline' onClick={() => fetchFunc(!remove)}>Undo</button>
</div>
);

toast.success(message, {
duration: 5000,
position: 'bottom-center',
icon: remove ? '➖' : '➕',
});
mutatePlayLater();
} else {
let resp;

try {
resp = await res.json();
} catch (e) {
console.error(e);
}

toast.error(resp?.error || 'Could not update Play Later', {
duration: 5000,
position: 'bottom-center',
});
}

setIsLoading(false);
};

const id = `play-later-btn-tooltip-${level._id.toString()}`;

return <>
<button
className={className}
data-tooltip-content={isInPlayLater ? 'Remove from Play Later' : 'Add to Play Later'}
data-tooltip-id={id}
disabled={isLoading}
onClick={async (e) => {
e.preventDefault();
fetchFunc(isInPlayLater);
}}
style={{
color: 'var(--color)',
}}
>
{isInPlayLater ?
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='w-6 h-6'>
<path strokeLinecap='round' strokeLinejoin='round' d='M19.5 12h-15' />
</svg>
:
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='w-6 h-6'>
<path strokeLinecap='round' strokeLinejoin='round' d='M12 4.5v15m7.5-7.5h-15' />
</svg>
}
</button>
<StyledTooltip id={id} />
</>;
}
26 changes: 22 additions & 4 deletions components/cards/selectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { AppContext } from '@root/contexts/appContext';
import classNames from 'classnames';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import Dimensions from '../../constants/dimensions';
import getPngDataClient from '../../helpers/getPngDataClient';
import SelectOption from '../../models/selectOption';
import SaveLevelToModal from '../modal/saveLevelToModal';
import { PlayLaterToggleButton } from './playLaterToggleButton';
import styles from './SelectCard.module.css';
import SelectCardContent from './selectCardContent';

Expand All @@ -14,6 +17,8 @@ interface SelectCardProps {

export default function SelectCard({ option, prefetch }: SelectCardProps) {
const [backgroundImage, setBackgroundImage] = useState<string>();
const [isSaveLevelToModalOpen, setIsSaveLevelToModalOpen] = useState(false);
const { user } = useContext(AppContext);

useEffect(() => {
if (option.level) {
Expand All @@ -26,10 +31,10 @@ export default function SelectCard({ option, prefetch }: SelectCardProps) {

return (
<div
className='p-3 overflow-hidden relative inline-block align-middle'
className='p-3 overflow-hidden relative inline-block align-middle select-card max-w-full'
key={`select-card-${option.id}`}
>
<div className='wrapper rounded-md overflow-hidden relative'
<div className='wrapper rounded-md overflow-hidden relative max-w-full'
style={{
height: option.height ?? Dimensions.OptionHeight,
width: option.width ?? Dimensions.OptionWidth,
Expand All @@ -48,7 +53,7 @@ export default function SelectCard({ option, prefetch }: SelectCardProps) {
{option.href ?
<Link
className={classNames(
'border-2 rounded-md items-center flex justify-center text-center',
'border-2 rounded-md items-center flex justify-center text-center max-w-full',
!option.disabled ? styles['card-border'] : undefined,
{ 'text-xl': !option.stats },
)}
Expand Down Expand Up @@ -86,6 +91,19 @@ export default function SelectCard({ option, prefetch }: SelectCardProps) {
<SelectCardContent option={option} />
</button>
}
{option.level && user && <>
<PlayLaterToggleButton className='absolute bottom-2 left-2 h-6 select-card-button' level={option.level} />
<button className='absolute bottom-2 right-2 select-card-button' onClick={() => setIsSaveLevelToModalOpen(true)}>
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='w-6 h-6' style={{ minWidth: 24, minHeight: 24 }}>
<path strokeLinecap='round' strokeLinejoin='round' d='M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z' />
</svg>
</button>
<SaveLevelToModal
closeModal={() => setIsSaveLevelToModalOpen(false)}
isOpen={isSaveLevelToModalOpen}
level={option.level}
/>
</>}
</div>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions components/cards/selectCardContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface SelectCardContentProps {
export default function SelectCardContent({ option }: SelectCardContentProps) {
return (<>
<div
className='font-bold break-words p-2'
className='font-bold break-words p-2 max-w-full'
style={{
width: option.width ?? Dimensions.OptionWidth,
}}
Expand All @@ -33,11 +33,11 @@ export default function SelectCardContent({ option }: SelectCardContentProps) {
/>
</div>
}
{option.stats && <div className='pt-1 italic'>{option.stats.getText()}</div>}
{!option.hideStats && option.stats && <div className='pt-1 italic'>{option.stats.getText()}</div>}
</div>
</div>
{option.stats?.isSolved() &&
<div className='absolute bottom-0 right-0'>
<div className='absolute top-0 right-0'>
<Solved />
</div>
}
Expand Down
2 changes: 1 addition & 1 deletion components/formatted/formattedDifficulty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default function FormattedDifficulty({ difficultyEstimate, id, uniqueUser
return (
<div className='flex justify-center difficultyText'>
<div data-tooltip-id={`difficulty-${id}`} data-tooltip-content={tooltip}>
<span className='text-md pr-1'>{difficulty.emoji}</span>
<span className='pr-1'>{difficulty.emoji}</span>
<span className='italic pr-1' style={{
color: color,
textShadow: '1px 1px black',
Expand Down
77 changes: 56 additions & 21 deletions components/header/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import DismissToast from '../toasts/dismissToast';
import MusicIcon from './musicIcon';

export default function Dropdown() {
const { forceUpdate, mutateUser, setShouldAttemptAuth, user, userLoading } = useContext(AppContext);
const { forceUpdate, mutateUser, playLater, setShouldAttemptAuth, user, userLoading } = useContext(AppContext);
const [isMusicModalOpen, setIsMusicModalOpen] = useState(false);
const [isThemeOpen, setIsThemeOpen] = useState(false);
const router = useRouter();
Expand Down Expand Up @@ -60,6 +60,10 @@ export default function Dropdown() {

const isLoggedIn = !userLoading && user;

function Divider() {
return <div className='opacity-30 m-1 h-px bg-4' />;
}

return (<>
<Menu>
<Menu.Button id='dropdownMenuBtn' aria-label='dropdown menu'>
Expand Down Expand Up @@ -100,15 +104,10 @@ export default function Dropdown() {
<span className='font-bold'>{user.score}</span>
<StyledTooltip id='levels-solved-dropdown' />
</div>
<div
className='opacity-30 m-1 h-px'
style={{
backgroundColor: 'var(--bg-color-4)',
}}
/>
<Divider />
</div>
}
{isLoggedIn && !isPro(user) &&
{isLoggedIn && !isPro(user) && <>
<Menu.Item>
{({ active }) => (
<Link href='/settings/pro' passHref>
Expand All @@ -124,7 +123,8 @@ export default function Dropdown() {
</Link>
)}
</Menu.Item>
}
<Divider />
</>}
<div className='block sm:hidden'>
<Menu.Item>
{({ active }) => (
Expand Down Expand Up @@ -162,6 +162,51 @@ export default function Dropdown() {
</Link>
)}
</Menu.Item>
{isPro(user) &&
<Menu.Item>
{({ active }) => (
<Link
href={`/collection/${user.name}/play-later`}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
if (!playLater || Object.keys(playLater).length === 0) {
toast.success('Add a level to your Play Later collection first!', { icon: '⚠️', duration: 3000 });
e.preventDefault();
}
}}
>
<div
className='flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3'
style={{
backgroundColor: active ? 'var(--bg-color-3)' : undefined,
}}
>
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' className='w-5 h-5' viewBox='0 0 16 16'>
<path d='M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z' />
<path d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z' />
</svg>
Play Later
</div>
</Link>
)}
</Menu.Item>
}
<Menu.Item>
{({ active }) => (
<Link href={`${getProfileSlug(user)}/collections`} passHref>
<div
className='flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3'
style={{
backgroundColor: active ? 'var(--bg-color-3)' : undefined,
}}
>
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='w-5 h-5'>
<path strokeLinecap='round' strokeLinejoin='round' d='M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z' />
</svg>
Collections
</div>
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link href={getProfileSlug(user)} passHref>
Expand All @@ -179,12 +224,7 @@ export default function Dropdown() {
</Link>
)}
</Menu.Item>
<div
className='opacity-30 m-1 h-px'
style={{
backgroundColor: 'var(--bg-color-4)',
}}
/>
<Divider />
</>}
<Menu.Item>
{({ active }) => (
Expand Down Expand Up @@ -243,12 +283,7 @@ export default function Dropdown() {
</Link>
)}
</Menu.Item>
<div
className='opacity-30 m-1 h-px'
style={{
backgroundColor: 'var(--bg-color-4)',
}}
/>
<Divider />
<Menu.Item>
{({ active }) => (
<div
Expand Down
2 changes: 1 addition & 1 deletion components/homepage/recommendedLevel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface RecommendedLevelProps {
id: string;
level?: EnrichedLevel | null;
onClick?: (option: SelectOption) => void;
title: JSX.Element | string;
title: React.ReactNode;
tooltip?: string;
}

Expand Down
2 changes: 1 addition & 1 deletion components/homepage/tutorial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ export default function Tutorial() {
}}>
Now <Link href='/signup' className='font-bold text-blue-500 hover:text-blue-400'>sign up</Link> for free to explore the world of Pathology!
</div>
<div className='text-md fadeIn' style={{
<div className='fadeIn' style={{
pointerEvents: 'all',
animationDelay: '2.5s'
}}>
Expand Down
Loading

0 comments on commit 524b867

Please sign in to comment.