Skip to content

Commit

Permalink
better actions for articles
Browse files Browse the repository at this point in the history
  • Loading branch information
mruwnik committed Feb 11, 2024
1 parent 12fe883 commit 8c78125
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 87 deletions.
9 changes: 9 additions & 0 deletions app/components/Article/article.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ article .link-popup p {
align-items: center;
gap: var(--spacing-32);
}

article .footer-comtainer {
display: flex;
align-items: center;
}

article .footer-comtainer > * {
margin: var(--spacing-8, 8px);
}
75 changes: 61 additions & 14 deletions app/components/Article/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {ReactNode, useRef, useEffect} from 'react'
import {useRef, useState, useEffect} from 'react'
import CopyIcon from '~/components/icons-generated/Copy'
import EditIcon from '~/components/icons-generated/Pencil'
import ThumbUpIcon from '~/components/icons-generated/ThumbUp'
import ThumbDownIcon from '~/components/icons-generated/ThumbDown'
import Button, {CompositeButton} from '~/components/Button'
import type {Question, Glossary, PageId, GlossaryEntry} from '~/server-utils/stampy'
import './article.css'

Expand Down Expand Up @@ -137,12 +140,58 @@ const insertGlossary = (pageid: string, glossary: Glossary) => {
}
}

type ActionProps = {
hint: string
icon: ReactNode
action?: any
const ArticleFooter = (question: Question) => {
const date =
question.updatedAt &&
new Date(question.updatedAt).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
})

return (
<div className="footer-comtainer">
{date && <div className="grey"> {`Updated ${date}`}</div>}
<div className="flex-double">
<Button className="secondary" action={question.answerEditLink || ''} tooltip="Edit article">
<EditIcon className="no-fill" />
</Button>
</div>
<span>Did this page help you?</span>

<CompositeButton>
<Button className="secondary" action={() => alert('Like')}>
<ThumbUpIcon />
<span className="teal-500">Yes</span>
</Button>
<Button className="secondary" action={() => alert('Dislike')}>
<ThumbDownIcon />
<span className="teal-500">No</span>
</Button>
</CompositeButton>
</div>
)
}

const ArticleActions = ({answerEditLink}: Question) => {
const [tooltip, setTooltip] = useState('Copy link to clipboard')

const copyLink = () => {
navigator.clipboard.writeText(window.location.toString())
setTooltip('Copied link to clipboard')
setTimeout(() => setTooltip('Copy link to clipboard'), 1000)
}

return (
<CompositeButton>
<Button className="secondary" action={copyLink} tooltip={tooltip}>
<CopyIcon />
</Button>
<Button className="secondary" action={answerEditLink || ''} tooltip="Edit article">
<EditIcon className="no-fill" />
</Button>
</CompositeButton>
)
}
const Action = ({icon}: ActionProps) => <div className="interactive-option">{icon}</div>

const Contents = ({pageid, html, glossary}: {pageid: PageId; html: string; glossary: Glossary}) => {
const elementRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -178,34 +227,32 @@ type ArticleProps = {
}
export const Article = ({question, glossary}: ArticleProps) => {
const {title, text, pageid, tags} = question

const ttr = (text: string, rate = 160) => {
const time = text.split(' ')
return Math.ceil(time.length / rate) // ceil to avoid "0 min read"
}
const lastUpdated = '<last updated goes here>'

return (
<article className="article-container">
<h1 className="teal">{title}</h1>
<div className="article-meta">
<p className="grey">{ttr(text || '')} min read</p>

<div className="interactive-options bordered">
<Action icon={<CopyIcon />} hint="Copy to clipboard" />
<Action icon={<EditIcon className="no-fill" />} hint="Edit" />
</div>
<ArticleActions {...question} />
</div>

<Contents pageid={pageid} html={text || ''} glossary={glossary || {}} />

<ArticleFooter {...question} />
<hr />
<div className="article-tags">
{tags.map((tag) => (
<a key={tag} className="tag bordered" href={`/tags/${tag}`}>
<Button key={tag} className="primary" action={`/tags/${tag}`}>
{tag}
</a>
</Button>
))}
</div>
<div className="article-last-updated">{lastUpdated}</div>
</article>
)
}
Expand Down
60 changes: 60 additions & 0 deletions app/components/Button/button.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.button {
cursor: pointer;
display: inline-flex;
height: var(--spacing-56, 56px);
padding: 7px 21px;
Expand All @@ -7,6 +8,7 @@
gap: var(--spacing-4, 4px);
box-shadow: 0px 16px 40px 0px rgba(32, 44, 89, 0.05);
border-radius: 6px;
box-sizing: border-box;
}

.primary {
Expand Down Expand Up @@ -47,3 +49,61 @@
.secondary-alt:hover {
border: 1px solid var(--colors-teal-200, #a6d9d7);
}

/* #### Composite button #### */
.composite-button {
box-shadow: 0px 16px 40px rgba(175, 183, 194, 0.2);
}

.composite-button > .button {
border-radius: 0;
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
box-shadow: revert;
}

.composite-button > .button:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-left-width: 1px;
}

.composite-button > .button:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
border-right-width: 1px;
}

/* Ensure that there are no gaps on the sides of the composite button */
.composite-button > .button {
margin-left: -2px;
}

/* Make sure the first button doesn't have a negative margin */
.composite-button > .button:first-child {
margin-left: 0;
}

.composite-button .button:hover {
border-width: 1px;
border-width: 1px;
margin: 2px;
}

.tooltip::after {
content: attr(data-tooltip);
position: absolute;
transform: translateY(var(--spacing-56, 56px));
padding: 5px;
color: var(--colors-white);
background: var(--text-black);
background: #333;
border-radius: 5px;
display: none;
z-index: 1;
}

.tooltip:hover::after {
display: block;
}
13 changes: 9 additions & 4 deletions app/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ type ButtonProps = {
action?: string | (() => void)
children?: ReactNode
className?: string
tooltip?: string
}
const Button = ({children, action, className}: ButtonProps) => {
const classes = 'button ' + (className || '')
const Button = ({children, action, tooltip, className}: ButtonProps) => {
const classes = ['button', className, tooltip && 'tooltip'].filter((i) => i).join(' ')
if (typeof action === 'string') {
return (
<Link to={action} className={classes}>
<Link to={action} className={classes} data-tooltip={tooltip}>
{children}
</Link>
)
}
return (
<button className={classes} onClick={action}>
<button className={classes} onClick={action} data-tooltip={tooltip}>
{children}
</button>
)
}

export const CompositeButton = ({children}: {children: ReactNode}) => (
<div className="composite-button">{children}</div>
)

export default Button
19 changes: 0 additions & 19 deletions app/hooks/useGlossary.ts
Original file line number Diff line number Diff line change
@@ -1,19 +0,0 @@
import {useState, useEffect} from 'react'
import {fetchGlossary} from '~/routes/questions.glossary'
import type {Glossary} from '~/server-utils/stampy'

const useGlossary = () => {
const [glossary, setGlossary] = useState<Glossary>({})

useEffect(() => {
const getGlossary = async () => {
const {data} = await fetchGlossary()
setGlossary(data)
}
getGlossary()
}, [setGlossary])

return {glossary}
}

export default useGlossary
19 changes: 0 additions & 19 deletions app/hooks/useTags.ts
Original file line number Diff line number Diff line change
@@ -1,19 +0,0 @@
import {useState, useEffect} from 'react'
import {fetchTags} from '~/routes/tags.all'
import type {Tag} from '~/server-utils/stampy'

const useTags = () => {
const [tags, setTags] = useState<Tag[]>([])

useEffect(() => {
const getTags = async () => {
const {tags} = await fetchTags()
setTags(tags)
}
getTags()
}, [setTags])

return {tags}
}

export default useTags
31 changes: 4 additions & 27 deletions app/newRoot.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
display: flex;
}

.flex-double {
flex-grow: 2;
}

body,
input {
font-size: 18px;
Expand Down Expand Up @@ -234,33 +238,6 @@ a:not(:hover, :focus-visible) {
height: 16px;
}

.container-page-subtitle {
position: relative;
font-size: 22px;
letter-spacing: -0.02em;
line-height: 155%;
font-weight: 600;
color: var(--colors-cool-grey-600);
text-align: left;
}

.container-page-title {
position: relative;
font-size: 64px;
letter-spacing: -0.02em;
line-height: 125%;
font-weight: 600;
color: var(--text-black);
text-align: left;
display: inline-block;
width: 770px;
margin-top: 40px;
}

.container-page-title p {
margin: 0;
}

.widget-title {
margin-block-start: 0;
margin-block-end: 0px;
Expand Down
5 changes: 2 additions & 3 deletions app/routes/tags.all.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache'
export const loader = async ({request, params}: Parameters<LoaderFunction>[0]) => {
const {data: tags, timestamp} = await loadTags(request)

const tagId = params['*'] && Number(params['*'].split('/')[0])
const currentTag = tagId ? tags.find((tagData) => tagData.tagId === tagId) : tags[0]
const tagId = params['*'] && params['*'].split('/')[0]
const currentTag = tagId ? tags.find(({tagId: checkedId, name}) => [checkedId.toString(), name].includes(tagId)) : tags[0]

if (currentTag === undefined) {
throw new Response(null, {
Expand All @@ -26,7 +26,6 @@ export const fetchTags = () => {
const {data, timestamp} = json

reloadInBackgroundIfNeeded(url, timestamp)
console.log(data)

return data
})
Expand Down
3 changes: 2 additions & 1 deletion app/server-utils/stampy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type AnswersRow = CodaRowCommon & {
'Alternate Phrasings': string
'Related Answers': '' | Entity[]
'Related IDs': '' | string[]
'Doc Last Edited': '' | string
Tags: '' | Entity[]
Banners: '' | Entity[]
'Rich Text': string
Expand Down Expand Up @@ -283,11 +284,11 @@ const convertToQuestion = ({name, values, updatedAt} = {} as AnswersRow): Questi
}))
: [],
status: values['Status']?.name as QuestionStatus,
updatedAt,
alternatePhrasings: extractText(values['Alternate Phrasings']),
subtitle: extractText(values.Subtitle),
icon: extractText(values.Icon),
parents: !values.Parents ? [] : values.Parents?.map(({name}) => name),
updatedAt: updatedAt || values['Doc Last Edited'],
})

export const loadQuestionDetail = withCache('questionDetail', async (question: string) => {
Expand Down

0 comments on commit 8c78125

Please sign in to comment.