From 5c4f8fc9160418128ccd092c80e14952cd6b21d5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 28 May 2024 17:24:34 +0800 Subject: [PATCH] First pass at the just read home web ux --- .../icons/home/AddToLibraryActionIcon.tsx | 33 +++ .../elements/icons/home/ArchiveActionIcon.tsx | 47 +++ .../elements/icons/home/CommentActionIcon.tsx | 33 +++ .../elements/icons/home/RemoveActionIcon.tsx | 61 ++++ .../elements/icons/home/ShareActionIcon.tsx | 33 +++ .../web/components/tokens/stitches.config.ts | 2 + .../web/lib/networking/queries/useGetHome.tsx | 244 +++++++++++++++ packages/web/pages/justread/index.tsx | 278 ++++++++++++++++++ 8 files changed, 731 insertions(+) create mode 100644 packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx create mode 100644 packages/web/components/elements/icons/home/ArchiveActionIcon.tsx create mode 100644 packages/web/components/elements/icons/home/CommentActionIcon.tsx create mode 100644 packages/web/components/elements/icons/home/RemoveActionIcon.tsx create mode 100644 packages/web/components/elements/icons/home/ShareActionIcon.tsx create mode 100644 packages/web/lib/networking/queries/useGetHome.tsx create mode 100644 packages/web/pages/justread/index.tsx diff --git a/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx b/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx new file mode 100644 index 000000000..e42a1bb8a --- /dev/null +++ b/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx @@ -0,0 +1,33 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from '../IconProps' + +import React from 'react' + +export class AddToLibraryActionIcon extends React.Component { + render() { + const strokeColor = (this.props.color || '#D9D9D9').toString() + const backgroundColor = (this.props.color || '#3D3D3D').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/home/ArchiveActionIcon.tsx b/packages/web/components/elements/icons/home/ArchiveActionIcon.tsx new file mode 100644 index 000000000..d97f49e5d --- /dev/null +++ b/packages/web/components/elements/icons/home/ArchiveActionIcon.tsx @@ -0,0 +1,47 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from '../IconProps' + +import React from 'react' + +export class ArchiveActionIcon extends React.Component { + render() { + const strokeColor = (this.props.color || '#D9D9D9').toString() + const backgroundColor = (this.props.color || '#3D3D3D').toString() + + return ( + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/home/CommentActionIcon.tsx b/packages/web/components/elements/icons/home/CommentActionIcon.tsx new file mode 100644 index 000000000..d0d704821 --- /dev/null +++ b/packages/web/components/elements/icons/home/CommentActionIcon.tsx @@ -0,0 +1,33 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from '../IconProps' + +import React from 'react' + +export class CommentActionIcon extends React.Component { + render() { + const strokeColor = (this.props.color || '#D9D9D9').toString() + const backgroundColor = (this.props.color || '#3D3D3D').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/home/RemoveActionIcon.tsx b/packages/web/components/elements/icons/home/RemoveActionIcon.tsx new file mode 100644 index 000000000..ab55125b1 --- /dev/null +++ b/packages/web/components/elements/icons/home/RemoveActionIcon.tsx @@ -0,0 +1,61 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from '../IconProps' + +import React from 'react' + +export class RemoveActionIcon extends React.Component { + render() { + const strokeColor = (this.props.color || '#D9D9D9').toString() + const backgroundColor = (this.props.color || '#3D3D3D').toString() + + return ( + + + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/home/ShareActionIcon.tsx b/packages/web/components/elements/icons/home/ShareActionIcon.tsx new file mode 100644 index 000000000..75648581d --- /dev/null +++ b/packages/web/components/elements/icons/home/ShareActionIcon.tsx @@ -0,0 +1,33 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from '../IconProps' + +import React from 'react' + +export class ShareActionIcon extends React.Component { + render() { + const strokeColor = (this.props.color || '#D9D9D9').toString() + const backgroundColor = (this.props.color || '#3D3D3D').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index db11183dc..5a1908fbf 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -156,6 +156,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = readerFontHighContrast: 'black', readerTableHeader: '#FFFFFF', readerMargin: 'white', + readerTextSubtle: '#898989', // Avatar Fallback color avatarBg: '#FFEA9F', @@ -216,6 +217,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thTextSubtle: '#1E1E1E', thTextSubtle2: '#6A6968', thTextSubtle3: '#ADADAD', + thTextSubtle4: '#EDEDED', thBorderColor: '#E1E1E1', thBorderSubtle: '#EEEEEE', diff --git a/packages/web/lib/networking/queries/useGetHome.tsx b/packages/web/lib/networking/queries/useGetHome.tsx new file mode 100644 index 000000000..6d6b14dca --- /dev/null +++ b/packages/web/lib/networking/queries/useGetHome.tsx @@ -0,0 +1,244 @@ +import { gql } from 'graphql-request' +import useSWR from 'swr' +import { gqlFetcher, makeGqlFetcher, publicGqlFetcher } from '../networkHelpers' + +type HomeResult = { + home: { + edges?: HomeEdge[] + errorCodes?: string[] + } +} + +export type HomeItemResponse = { + error: boolean + isValidating: boolean + errorMessage?: string + sections?: HomeSection[] +} + +export type HomeItem = { + id: string + date: string + title: string + url: string + source: HomeItemSource + + previewContent?: string + wordCount?: number + + author?: string + broadcastCount?: number + canArchive?: boolean + canComment?: boolean + canDelete?: boolean + canSave?: boolean + canShare?: boolean + dir?: string + + likeCount?: number + saveCount?: number + seen_at?: string + thumbnail?: string +} + +type HomeItemSourceType = 'LIBRARY' | 'NEWSLETTER' | 'RECOMMENDATION' | 'RSS' + +export type HomeItemSource = { + name: string + type: HomeItemSourceType + id?: string + icon?: string + url?: string +} + +export type HomeSection = { + title: string + layout: string + items: HomeItem[] + thumbnail?: string +} + +type HomeEdge = { + cursor: string + node: HomeSection +} + +export function useGetHomeItems(): HomeItemResponse { + const query = gql` + query GetHomeItems($after: String, $first: Int) { + home(first: $first, after: $after) { + ... on HomeSuccess { + edges { + cursor + node { + title + layout + thumbnail + items { + id + title + url + thumbnail + previewContent + saveCount + likeCount + broadcastCount + date + author + dir + seen_at + wordCount + source { + id + name + url + icon + type + } + canSave + canComment + canShare + canArchive + canDelete + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + totalCount + } + } + ... on HomeError { + errorCodes + } + } + } + ` + + const variables = { + first: 3, + after: null, + } + + const { data, error, isValidating } = useSWR( + [query, variables.first, variables.after], + makeGqlFetcher(variables) + ) + + if (error) { + return { + error: true, + isValidating, + errorMessage: error.toString(), + } + } + + const result = data as HomeResult + console.log('result: ', result) + + if (result && result.home.errorCodes) { + const errorCodes = result.home.errorCodes + return { + error: true, + isValidating, + errorMessage: errorCodes.length > 0 ? errorCodes[0] : undefined, + } + } + + if (result && result.home && result.home.edges) { + return { + error: false, + isValidating, + sections: result.home.edges.map((edge) => { + return edge.node + }), + } + } + + return { + isValidating, + error: !!error, + } +} + +// Fake HomeSubscription data +const source1: HomeItemSource = { + id: 'sub1', + name: 'Tech News', + type: 'NEWSLETTER', + icon: 'https://example.com/icon1.png', + url: 'https://example.com/tech-news', +} + +const source2: HomeItemSource = { + id: 'sub2', + name: 'Daily Updates', + type: 'RSS', + icon: 'https://example.com/icon2.png', + url: 'https://example.com/daily-updates', +} + +// Fake HomeItem data +export const stubHomeItems: HomeItem[] = [ + { + id: 'item1', + date: '2024-05-01', + title: 'Breaking Tech News', + url: 'https://example.com/article1', + source: source1, + previewContent: 'A brief summary of the tech news...', + wordCount: 500, + author: 'John Doe', + broadcastCount: 10, + canArchive: true, + canComment: true, + canDelete: true, + canSave: true, + canShare: true, + dir: 'ltr', + likeCount: 100, + saveCount: 50, + seen_at: '2024-05-02', + thumbnail: 'https://example.com/thumbnail1.png', + }, + { + id: 'item2', + date: '2024-05-03', + title: 'Daily Updates: May 3rd', + url: 'https://example.com/article2', + source: source2, + previewContent: 'A brief summary of the daily updates...', + wordCount: 300, + author: 'Jane Smith', + broadcastCount: 5, + canArchive: false, + canComment: true, + canDelete: false, + canSave: true, + canShare: false, + dir: 'ltr', + likeCount: 75, + saveCount: 30, + seen_at: '2024-05-04', + thumbnail: 'https://example.com/thumbnail2.png', + }, + { + id: 'item3', + date: '2024-05-05', + title: 'In-Depth Analysis', + url: 'https://example.com/article3', + source: source1, + wordCount: 1500, + canArchive: true, + canComment: true, + canDelete: true, + canSave: true, + canShare: true, + likeCount: 200, + saveCount: 120, + seen_at: '2024-05-06', + }, +] diff --git a/packages/web/pages/justread/index.tsx b/packages/web/pages/justread/index.tsx new file mode 100644 index 000000000..4d32c16d7 --- /dev/null +++ b/packages/web/pages/justread/index.tsx @@ -0,0 +1,278 @@ +import { styled } from '@stitches/react' +import { AddToLibraryActionIcon } from '../../components/elements/icons/home/AddToLibraryActionIcon' +import { ArchiveActionIcon } from '../../components/elements/icons/home/ArchiveActionIcon' +import { CommentActionIcon } from '../../components/elements/icons/home/CommentActionIcon' +import { RemoveActionIcon } from '../../components/elements/icons/home/RemoveActionIcon' +import { ShareActionIcon } from '../../components/elements/icons/home/ShareActionIcon' +import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme' +import { + HStack, + SpanBox, + VStack, +} from './../../components/elements/LayoutPrimitives' + +import * as HoverCard from '@radix-ui/react-hover-card' +import { Button } from '../../components/elements/Button' +import { + HomeItem, + HomeSection, + stubHomeItems, + useGetHomeItems, +} from '../../lib/networking/queries/useGetHome' +import { timeAgo } from '../../components/patterns/LibraryCards/LibraryCardStyles' +import { theme } from '../../components/tokens/stitches.config' + +export default function Home(): JSX.Element { + const homeData = useGetHomeItems() + console.log('home sections: ', homeData.sections) + useApplyLocalTheme() + + return ( + + + {homeData.sections?.map((homeSection, idx) => { + switch (homeSection.layout) { + case 'long': + return ( + + ) + } + })} + + + ) +} + +type HomeSectionProps = { + homeSection: HomeSection +} + +const LongHomeSection = (props: HomeSectionProps): JSX.Element => { + return ( + + {props.homeSection.items.map((homeItem) => { + return + })} + + ) +} + +const CoverImage = styled('img', { + objectFit: 'cover', +}) + +type HomeItemViewProps = { + homeItem: HomeItem +} + +const TimeAgo = (props: HomeItemViewProps): JSX.Element => { + return ( + + {timeAgo(props.homeItem.date)} + + ) +} + +const Title = (props: HomeItemViewProps): JSX.Element => { + return ( + + {props.homeItem.title} + + ) +} + +const HomeItemView = (props: HomeItemViewProps): JSX.Element => { + return ( + + + + + + + + + </VStack> + <SpanBox css={{ ml: 'auto' }}> + {props.homeItem.thumbnail && ( + <CoverImage + css={{ + mt: '6px', + width: '120px', + height: '70px', + borderRadius: '4px', + }} + src={props.homeItem.thumbnail} + ></CoverImage> + )} + </SpanBox> + </HStack> + <SpanBox + css={{ fontFamily: '$inter', fontSize: '13px', lineHeight: '23px' }} + > + {props.homeItem.previewContent} + </SpanBox> + <HStack css={{ gap: '6px', mt: '20px' }}> + <Button style="ghost"> + <AddToLibraryActionIcon /> + </Button> + <Button style="ghost"> + <CommentActionIcon /> + </Button> + <Button style="ghost"> + <ShareActionIcon /> + </Button> + <Button style="ghost"> + <ArchiveActionIcon /> + </Button> + <Button style="ghost"> + <RemoveActionIcon /> + </Button> + </HStack> + </VStack> + ) +} + +const SiteIconSmall = styled('img', { + width: '16px', + height: '16px', + borderRadius: '100px', +}) + +const SiteIconLarge = styled('img', { + width: '25px', + height: '25px', + borderRadius: '100px', +}) + +const SourceInfo = (props: HomeItemViewProps) => ( + <HoverCard.Root> + <HoverCard.Trigger asChild> + <HStack + distribution="start" + alignment="center" + css={{ gap: '5px', cursor: 'pointer' }} + > + <SiteIconSmall + className="Image normal" + src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png" + alt={props.homeItem.source.name} + /> + <HStack + css={{ + lineHeight: '1', + pb: '3px', + fontFamily: '$inter', + fontWeight: '500', + fontSize: '13px', + color: '$readerFont', + textDecoration: 'underline', + }} + > + {props.homeItem.source.name} + </HStack> + </HStack> + </HoverCard.Trigger> + <HoverCard.Portal> + <HoverCard.Content sideOffset={5}> + <VStack + alignment="start" + distribution="start" + css={{ + width: '380px', + height: '200px', + bg: '$thBackground2', + borderRadius: '10px', + padding: '15px', + gap: '10px', + boxShadow: theme.shadows.cardBoxShadow.toString(), + }} + > + <HStack + distribution="start" + alignment="center" + css={{ width: '100%', gap: '10px' }} + > + <SiteIconLarge + className="Image normal" + src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png" + alt={props.homeItem.source.name} + /> + <SpanBox + css={{ + fontFamily: '$inter', + fontWeight: '500', + fontSize: '14px', + }} + > + {props.homeItem.source.name} + </SpanBox> + <SpanBox css={{ ml: 'auto' }}> + <Button style="ctaBlue">+ Follow</Button> + </SpanBox> + </HStack> + <SpanBox + css={{ + fontFamily: '$inter', + fontSize: '13px', + color: '$thTextSubtle4', + }} + > + The description of the newsletter or RSS feed, this would lazy load. + </SpanBox> + </VStack> + <HoverCard.Arrow fill={theme.colors.thBackground2.toString()} /> + </HoverCard.Content> + </HoverCard.Portal> + </HoverCard.Root> +)