Skip to content

Commit

Permalink
Merge pull request #44 from prtcl/spike/node-transition-viewer
Browse files Browse the repository at this point in the history
spike/node transition viewer
  • Loading branch information
prtcl authored Jan 3, 2025
2 parents 3675acb + f129403 commit c839f8b
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 809 deletions.
14 changes: 0 additions & 14 deletions convex/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ export const loadProjects = query({
},
});

export const loadProjectIds = query({
args: {},
handler: async (ctx) => {
const projects = await ctx.db
.query('projects')
.withIndex('deletedByOrder', (q) => q.eq('deletedAt', null))
.filter((q) => q.neq(q.field('publishedAt'), null))
.order('asc')
.collect();

return projects?.map((p) => p._id);
},
});

const getProjectOrNotFound = async (
ctx: QueryCtx,
projectId: Id<'projects'>,
Expand Down
83 changes: 59 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Box, Stack } from 'styled-system/jsx';
import { api } from '~/convex/api';
import { ProjectItem, useProjectViewer } from '~/feat/Projects';
import { Visualization } from '~/feat/Visualization';
import { FeatureFlags, useFeatureFlags } from '~/lib/features';
import { VizContainer, ContentOverlay, Root } from '~/lib/layout';
import { useBreakpoints, useInteractions } from '~/lib/viewport';
import { Button } from '~/ui/Button';
import { Link } from '~/ui/Link';
import { Text } from '~/ui/Text';
import { FeatureFlags, useFeatureFlags } from './lib/features';

const Bio = () => {
return (
Expand Down Expand Up @@ -40,11 +41,34 @@ const LoadMore = (props: PropsWithChildren & { onClick: () => void }) => (
</Button>
);

const ContentContainer = (
props: PropsWithChildren & { state: 'foreground' | 'background' },
) => {
const { children, state } = props;
return (
<Box
transition={`
opacity 168ms cubic-bezier(.79,.31,.59,.83),
scale 336ms cubic-bezier(.79,.31,.59,.83)
`}
width="100%"
height="100%"
{...(state === 'background'
? { opacity: 0.5, scale: 0.99 }
: { opacity: 1, scale: 1 })}
>
{children}
</Box>
);
};

const LOAD_ITEMS_COUNT = 7;

const App = () => {
const { features } = useFeatureFlags();
const { openProjectViewer } = useProjectViewer();
const { isMobile } = useBreakpoints();
const { hasTouch } = useInteractions();
const { isOpen, openProjectViewer, projectId } = useProjectViewer();
const {
results: projects,
status,
Expand All @@ -64,29 +88,40 @@ const App = () => {
</VizContainer>
{projects && !isLoading && (
<ContentOverlay animation="fade-in 340ms linear">
<Stack direction="column" gap={4} px={[3, 4]} pt={8} pb={12}>
<Bio />
<Stack gap={2}>
{projects.map((project) => {
return (
<ProjectItem
key={project._id}
isPreviewEnabled={features.get(
FeatureFlags.PROJECT_PREVIEWS,
)}
isViewerEnabled={features.get(FeatureFlags.PROJECT_VIEWER)}
item={project}
onSelect={(projectId) => openProjectViewer(projectId)}
/>
);
})}
{canLoadMore && (
<LoadMore onClick={() => loadMore(LOAD_ITEMS_COUNT)}>
More...
</LoadMore>
)}
<ContentContainer state={isOpen ? 'background' : 'foreground'}>
<Stack direction="column" gap={4} px={[3, 4]} pt={8} pb={12}>
<Bio />
<Stack gap={2}>
{projects.map((project) => {
return (
<ProjectItem
key={project._id}
isSelected={isOpen && projectId === project._id}
isViewerEnabled={features.get(
FeatureFlags.PROJECT_VIEWER,
)}
item={project}
onSelect={(_, target) => {
if ((isMobile || hasTouch) && !!project.embedId) {
openProjectViewer(
project,
target.getBoundingClientRect(),
);
} else {
window.open(project.url, '_blank');
}
}}
/>
);
})}
{canLoadMore && (
<LoadMore onClick={() => loadMore(LOAD_ITEMS_COUNT)}>
More...
</LoadMore>
)}
</Stack>
</Stack>
</Stack>
</ContentContainer>
</ContentOverlay>
)}
</Root>
Expand Down
19 changes: 2 additions & 17 deletions src/feat/Projects/ProjectProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import { type PropsWithChildren } from 'react';
import { ProjectViewer } from './ProjectViewer';
import { NextPrevContext, useNextPrevApi } from './hooks/useNextPrev';
import {
ProjectViewerContext,
useProjectViewerState,
} from './hooks/useProjectViewer';

const ProjectViewerProvider = (props: PropsWithChildren) => (
export const ProjectProvider = (props: PropsWithChildren) => (
<ProjectViewerContext.Provider value={useProjectViewerState()}>
{props.children}
<ProjectViewer />
</ProjectViewerContext.Provider>
);

const NextPrevProvider = (props: PropsWithChildren) => (
<NextPrevContext.Provider value={useNextPrevApi()}>
{props.children}
</NextPrevContext.Provider>
);

export const ProjectProvider = (props: PropsWithChildren) => (
<ProjectViewerProvider>
<NextPrevProvider>
{props.children}
<ProjectViewer />
</NextPrevProvider>
</ProjectViewerProvider>
);
148 changes: 47 additions & 101 deletions src/feat/Projects/ProjectViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,59 @@
import { useQuery } from 'convex/react';
import { useState } from 'react';
import useKey from 'react-use/lib/useKey';
import { Flex, Stack, VisuallyHidden } from 'styled-system/jsx';
import { api } from '~/convex/api';
import { forwardRef } from 'react';
import { createPortal } from 'react-dom';
import { Flex, type FlexProps } from 'styled-system/jsx';
import { ErrorBoundary } from '~/lib/errors';
import * as Dialog from '~/ui/Dialog';
import { IconButton } from '~/ui/IconButton';
import { Text } from '~/ui/Text';
import { BackIcon, ChevronLeftIcon } from '~/ui/icons';
import {
PanelContainer,
PanelFooter,
PanelHeader,
} from './components/PanelContainer';
import { ProjectDetails } from './components/ProjectDetails';
import { ProjectUrl } from './components/ProjectUrl';
import { useNextPrev, type Directions } from './hooks/useNextPrev';
import { useProjectViewer } from './hooks/useProjectViewer';
import { useProjectViewer, ViewerType } from './hooks/useProjectViewer';
import { EmbedViewer } from './ui/EmbedViewer';

const DetailsPanel = () => {
const { projectId, updateProjectId } = useProjectViewer();
const project = useQuery(api.projects.loadProject, { projectId });
const { next, prev } = useNextPrev(projectId);
const [direction, setDirection] = useState<Directions>(0);

const handleLeft = () => {
if (prev) {
updateProjectId(prev);
setDirection(-1);
}
};
const handleRight = () => {
if (next) {
updateProjectId(next);
setDirection(1);
const Overlay = forwardRef<HTMLDivElement, FlexProps>(
function Overlay(props, ref) {
const { children, ...flexProps } = props;
const { closeViewer, isOpen } = useProjectViewer();
if (!isOpen) {
return null;
}
};
useKey('ArrowLeft', handleLeft, {}, [prev]);
useKey('ArrowRight', handleRight, {}, [next]);

return (
<>
<Flex direction="column" flex={1}>
<ProjectDetails direction={direction} projectId={projectId} />
<PanelFooter>
<ProjectUrl url={project?.url} direction={direction} />
<Stack direction="row" gap={[0, 1]}>
<IconButton onPress={() => handleLeft()} isDisabled={prev === null}>
<ChevronLeftIcon />
</IconButton>
<IconButton
onPress={() => handleRight()}
isDisabled={next === null}
>
<ChevronLeftIcon transform="rotate(180deg)" />
</IconButton>
</Stack>
</PanelFooter>
return (
<Flex
ref={ref}
position="fixed"
inset={0}
onClick={() => closeViewer()}
onTouchMove={() => closeViewer()}
{...flexProps}
>
{children}
</Flex>
<VisuallyHidden>
<Dialog.Title>{project?.title}</Dialog.Title>
</VisuallyHidden>
</>
);
};

const NotFound = () => (
<Flex alignItems="center" justifyContent="center" flex={1} width="100%">
<Text>Not Found</Text>
</Flex>
);
},
);

export const ProjectViewer = () => {
const { isOpen, closeViewer } = useProjectViewer();
const NotFound = () =>
createPortal(
<Overlay alignItems="center" bg="zinc.100/68" justifyContent="center">
<Text>Not Found</Text>
</Overlay>,
document.body,
);

export const ProjectViewer = () => {
const { isOpen, projectId, viewerType, origin } = useProjectViewer();
return (
<Dialog.Root isOpen={isOpen} onClose={closeViewer}>
<Dialog.Overlay />
<Dialog.Content
borderRadius={[16, 16, 16, 12]}
height="100%"
mt="calc(env(safe-area-inset-bottom, 0px) / 2 * -1)"
maxHeight={[
'calc(97% - (env(safe-area-inset-bottom, 0px) * 2))',
'calc(97% - (env(safe-area-inset-bottom, 0px) * 2))',
'calc(96% - (env(safe-area-inset-bottom, 0px) * 2))',
'92vh',
'84vh',
]}
maxWidth={['97%', '95%', '95vw', '92vw', '82vw', '74vw', '80rem']}
onOpenAutoFocus={(e) => e.preventDefault()}
overflow="hidden"
shadow="2xl"
width="100%"
>
<PanelContainer>
<PanelHeader>
<IconButton ml={-1} mt={-1} onPress={() => closeViewer()} size="md">
<BackIcon
color={['zinc.500', 'zinc.500', 'zinc.400', 'zinc.300']}
size="md"
/>
</IconButton>
</PanelHeader>
<ErrorBoundary fallback={() => <NotFound />}>
<DetailsPanel />
</ErrorBoundary>
</PanelContainer>
</Dialog.Content>
</Dialog.Root>
<ErrorBoundary fallback={() => <NotFound />}>
{createPortal(
<>
<Overlay />
{[ViewerType.SOUND, ViewerType.VIDEO].includes(viewerType) && (
<EmbedViewer
projectId={projectId}
isOpen={isOpen}
origin={origin}
/>
)}
</>,
document.body,
)}
</ErrorBoundary>
);
};
67 changes: 0 additions & 67 deletions src/feat/Projects/components/PanelContainer.tsx

This file was deleted.

Loading

0 comments on commit c839f8b

Please sign in to comment.