diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-01-15 22:45:57 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-01-15 22:51:30 +0100 |
| commit | aa1ca65e7c9807c6d6020e39166536297fe1cdae (patch) | |
| tree | 2648da350fec3b71ab7f575d63e4c63ba08248b1 /src/components/Widgets | |
| parent | 16dbb4742264edac82fa6bb8e461259d097f4437 (diff) | |
chore: update sidebar and widgets styles
I'm now using a widget that can be expanded/collapsed. It also allows
me to handle more effectively widgets overflow and to avoid styles
repetitions.
However, with stylelint rule "no-descending-specificity", I'm not sure
if the stylesheets are really logical... Maybe I should deactivate this
rule.
Diffstat (limited to 'src/components/Widgets')
| -rw-r--r-- | src/components/Widgets/CVPreview/CVPreview.module.scss | 6 | ||||
| -rw-r--r-- | src/components/Widgets/CVPreview/CVPreview.tsx | 36 | ||||
| -rw-r--r-- | src/components/Widgets/RecentPosts/RecentPosts.module.scss | 94 | ||||
| -rw-r--r-- | src/components/Widgets/RecentPosts/RecentPosts.tsx | 63 | ||||
| -rw-r--r-- | src/components/Widgets/RelatedThematics/RelatedThematics.tsx | 31 | ||||
| -rw-r--r-- | src/components/Widgets/RelatedTopics/RelatedTopics.tsx | 31 | ||||
| -rw-r--r-- | src/components/Widgets/Sharing/Sharing.module.scss | 196 | ||||
| -rw-r--r-- | src/components/Widgets/Sharing/Sharing.tsx | 113 | ||||
| -rw-r--r-- | src/components/Widgets/SocialMedia/SocialMedia.module.scss | 48 | ||||
| -rw-r--r-- | src/components/Widgets/SocialMedia/SocialMedia.tsx | 72 | ||||
| -rw-r--r-- | src/components/Widgets/ThematicsList/ThematicsList.tsx | 46 | ||||
| -rw-r--r-- | src/components/Widgets/ToC/ToC.module.scss | 70 | ||||
| -rw-r--r-- | src/components/Widgets/ToC/ToC.tsx | 30 | ||||
| -rw-r--r-- | src/components/Widgets/TopicsList/TopicsList.tsx | 46 | ||||
| -rw-r--r-- | src/components/Widgets/index.tsx | 21 |
15 files changed, 903 insertions, 0 deletions
diff --git a/src/components/Widgets/CVPreview/CVPreview.module.scss b/src/components/Widgets/CVPreview/CVPreview.module.scss new file mode 100644 index 0000000..6ddd696 --- /dev/null +++ b/src/components/Widgets/CVPreview/CVPreview.module.scss @@ -0,0 +1,6 @@ +.preview { + position: relative; + width: 100%; + height: 20rem; + margin-bottom: var(--spacing-sm); +} diff --git a/src/components/Widgets/CVPreview/CVPreview.tsx b/src/components/Widgets/CVPreview/CVPreview.tsx new file mode 100644 index 0000000..e52a9b2 --- /dev/null +++ b/src/components/Widgets/CVPreview/CVPreview.tsx @@ -0,0 +1,36 @@ +import { ExpandableWidget } from '@components/WidgetParts'; +import { Trans } from '@lingui/macro'; +import Image from 'next/image'; +import Link from 'next/link'; +import styles from './CVPreview.module.scss'; + +const CVPreview = ({ + title, + imgSrc, + pdf, +}: { + title: string; + imgSrc: string; + pdf: string; +}) => { + return ( + <ExpandableWidget title={title} expand={true}> + <div className={styles.preview}> + <Image + src={imgSrc} + layout="fill" + objectFit="contain" + objectPosition="left" + alt="CV Armand Philippot" + /> + </div> + <p> + <Trans> + Download <Link href={pdf}>CV in PDF</Link> + </Trans> + </p> + </ExpandableWidget> + ); +}; + +export default CVPreview; diff --git a/src/components/Widgets/RecentPosts/RecentPosts.module.scss b/src/components/Widgets/RecentPosts/RecentPosts.module.scss new file mode 100644 index 0000000..95ad7fe --- /dev/null +++ b/src/components/Widgets/RecentPosts/RecentPosts.module.scss @@ -0,0 +1,94 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.list { + @extend %flex-list; + align-items: stretch; + gap: var(--spacing-xs); + margin-bottom: var(--spacing-md); +} + +.item { + width: min(calc(100vw - var(--spacing-md)), 25ch); + text-align: center; +} + +.article { + display: flex; + flex-flow: column nowrap; + height: 100%; + padding: 0 0 var(--spacing-md); +} + +.title { + flex: 1; + margin: var(--spacing-sm) 0; + padding: 0 var(--spacing-md); + text-decoration: underline solid transparent 0; + transition: all 0.3s linear 0s; +} + +.link { + display: block; + height: 100%; + background: var(--color-bg); + color: inherit; + text-decoration: none; + border: fun.convert-px(3) solid var(--color-primary); + border-radius: fun.convert-px(5); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow-light); + transition: all 0.3s ease-in-out 0s; + + &:hover, + &:focus, + &:active { + color: inherit; + } + + &:hover, + &:focus { + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-lighter), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow-lighter), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow-lighter), + fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3) + var(--color-shadow-lighter); + transform: scale(1.05); + } + + &:focus { + .title { + text-decoration: underline solid var(--color-primary) 0.3ex; + } + } + + &:active { + box-shadow: 0 0 0 0 var(--color-shadow-light); + transform: scale(0.95); + + .title { + text-decoration: none; + } + } +} + +.cover { + width: 100%; + height: clamp(fun.convert-px(100), 20vw, fun.convert-px(150)); + position: relative; + border: fun.convert-px(1) solid var(--color-border-light); +} + +.meta { + display: block; + margin: 0; + padding: 0 var(--spacing-md); + font-size: var(--font-size-sm); +} diff --git a/src/components/Widgets/RecentPosts/RecentPosts.tsx b/src/components/Widgets/RecentPosts/RecentPosts.tsx new file mode 100644 index 0000000..1569284 --- /dev/null +++ b/src/components/Widgets/RecentPosts/RecentPosts.tsx @@ -0,0 +1,63 @@ +import { t } from '@lingui/macro'; +import { getPublishedPosts } from '@services/graphql/queries'; +import { ArticlePreview } from '@ts/types/articles'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import useSWR from 'swr'; +import styles from './RecentPosts.module.scss'; + +const RecentPosts = () => { + const { data, error } = useSWR('/recent-posts', () => + getPublishedPosts({ first: 3 }) + ); + const { locale } = useRouter(); + const dateOptions: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'long', + year: 'numeric', + }; + + const getPost = (post: ArticlePreview) => { + return ( + <li key={post.id} className={styles.item}> + <Link href={`/article/${post.slug}`}> + <a className={styles.link}> + <article className={styles.article}> + {post.featuredImage && + Object.keys(post.featuredImage).length > 0 && ( + <div className={styles.cover}> + <Image + src={post.featuredImage.sourceUrl} + alt={post.featuredImage.altText} + layout="fill" + objectFit="contain" + /> + </div> + )} + <h3 className={styles.title}>{post.title}</h3> + <dl className={styles.meta}> + <dt>{t`Published on:`}</dt> + <dd> + {new Date(post.dates.publication).toLocaleDateString( + locale, + dateOptions + )} + </dd> + </dl> + </article> + </a> + </Link> + </li> + ); + }; + + if (error) return <div>{t`Failed to load.`}</div>; + if (!data) return <div>{t`Loading...`}</div>; + + return ( + <ul className={styles.list}>{data.posts.map((post) => getPost(post))}</ul> + ); +}; + +export default RecentPosts; diff --git a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx new file mode 100644 index 0000000..afe3460 --- /dev/null +++ b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx @@ -0,0 +1,31 @@ +import { ExpandableWidget, List } from '@components/WidgetParts'; +import { t } from '@lingui/macro'; +import { ThematicPreview } from '@ts/types/taxonomies'; +import Link from 'next/link'; + +const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => { + const sortedThematics = [...thematics].sort((a, b) => + a.title.localeCompare(b.title) + ); + + const thematicsList = sortedThematics.map((thematic) => { + return ( + <li key={thematic.databaseId}> + <Link href={`/thematique/${thematic.slug}`}> + <a>{thematic.title}</a> + </Link> + </li> + ); + }); + + return ( + <ExpandableWidget + title={thematics.length > 1 ? t`Related thematics` : t`Related thematic`} + withBorders={true} + > + <List items={thematicsList} /> + </ExpandableWidget> + ); +}; + +export default RelatedThematics; diff --git a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx new file mode 100644 index 0000000..aab8cc1 --- /dev/null +++ b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx @@ -0,0 +1,31 @@ +import { ExpandableWidget, List } from '@components/WidgetParts'; +import { t } from '@lingui/macro'; +import { SubjectPreview } from '@ts/types/taxonomies'; +import Link from 'next/link'; + +const RelatedTopics = ({ topics }: { topics: SubjectPreview[] }) => { + const sortedSubjects = [...topics].sort((a, b) => + a.title.localeCompare(b.title) + ); + + const subjects = sortedSubjects.map((subject) => { + return ( + <li key={subject.databaseId}> + <Link href={`/sujet/${subject.slug}`}> + <a>{subject.title}</a> + </Link> + </li> + ); + }); + + return ( + <ExpandableWidget + title={topics.length > 1 ? t`Related topics` : t`Related topic`} + withBorders={true} + > + <List items={subjects} /> + </ExpandableWidget> + ); +}; + +export default RelatedTopics; diff --git a/src/components/Widgets/Sharing/Sharing.module.scss b/src/components/Widgets/Sharing/Sharing.module.scss new file mode 100644 index 0000000..3477c88 --- /dev/null +++ b/src/components/Widgets/Sharing/Sharing.module.scss @@ -0,0 +1,196 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; +@use "@styles/abstracts/placeholders"; + +.list { + @extend %flex-list; + + gap: var(--spacing-sm); + padding: var(--spacing-2xs) 0 0 var(--spacing-2xs); + + @include mix.media("screen") { + @include mix.dimensions("md") { + gap: var(--spacing-xs); + width: min-content; + } + } +} + +.link { + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: var(--spacing-2xs) var(--spacing-xs); + border-radius: fun.convert-px(3); + color: var(--color-fg-inverted); + font-weight: 600; + text-decoration: none; + text-shadow: fun.convert-px(2) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-dark); + transition: all 0.3s ease-in-out 0s; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + font-size: var(--font-size-sm); + } + } + + &:hover, + &:focus { + color: hsl(0, 0%, 100%); + transform: translateX(#{fun.convert-px(-3)}) + translateY(#{fun.convert-px(-3)}); + + @include mix.motion("reduce") { + text-decoration: underline; + } + } + + &:active { + color: hsl(0, 0%, 100%); + transform: translateX(#{fun.convert-px(2)}) translateY(#{fun.convert-px(2)}); + + @include mix.motion("reduce") { + transform: none; + } + } + + &::before { + display: block; + background-repeat: no-repeat; + content: ""; + filter: drop-shadow( + #{fun.convert-px(1)} #{fun.convert-px(1)} #{fun.convert-px(1)} hsl(0, 0%, 0%) + ); + width: fun.convert-px(30); + height: fun.convert-px(30); + } + + &--diaspora { + background: hsl(0, 0%, 13%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(0, 0%, 3%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 hsl(0, 0%, 3%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 hsl(0, 0%, 3%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path style="fill:#ffffff;" d="M15.257 21.928l-2.33-3.255c-.622-.87-1.128-1.549-1.155-1.55-.027 0-1.007 1.317-2.317 3.115-1.248 1.713-2.28 3.115-2.292 3.115-.035 0-4.5-3.145-4.51-3.178-.006-.016 1.003-1.497 2.242-3.292 1.239-1.794 2.252-3.29 2.252-3.325 0-.056-.401-.197-3.55-1.247a1604.93 1604.93 0 01-3.593-1.2c-.033-.013.153-.635.79-2.648.46-1.446.845-2.642.857-2.656.013-.015 1.71.528 3.772 1.207 2.062.678 3.766 1.233 3.787 1.233.021 0 .045-.032.053-.07.008-.039.026-1.794.04-3.902.013-2.107.036-3.848.05-3.87.02-.03.599-.038 2.725-.038 1.485 0 2.716.01 2.735.023.023.016.064 1.175.132 3.776.112 4.273.115 4.33.183 4.33.026 0 1.66-.547 3.631-1.216 1.97-.668 3.593-1.204 3.605-1.191.04.045 1.656 5.307 1.636 5.327-.011.01-1.656.574-3.655 1.252-2.75.932-3.638 1.244-3.645 1.284-.006.029.94 1.442 2.143 3.202 1.184 1.733 2.148 3.164 2.143 3.18-.012.036-4.442 3.299-4.48 3.299-.015 0-.577-.767-1.249-1.705z"/></svg>' + )); + } + } + + &--email { + background: hsl(0, 0%, 44%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(0, 0%, 34%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 hsl(0, 0%, 34%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 hsl(0, 0%, 34%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill:#ffffff;" d="M15.909 12.123L24 17.238V6.792zM0 6.792v10.446l8.091-5.115zM22.5 3.75h-21c-.748 0-1.343.558-1.455 1.276L12 12.904l11.955-7.877c-.112-.718-.706-1.276-1.455-1.276zm-7.965 9.279l-2.123 1.398a.75.75 0 01-.825 0l-2.122-1.4-9.417 5.957c.116.712.707 1.266 1.452 1.266h21c.746 0 1.337-.553 1.452-1.266z"/></svg>' + )); + } + } + + &--facebook { + background: hsl(214, 89%, 52%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(214, 89%, 42%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 + hsl(214, 89%, 42%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 + hsl(214, 89%, 42%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill:#ffffff;" d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>' + )); + } + } + + &--journal-du-hacker { + background: hsl(210, 24%, 51%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(210, 24%, 41%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 + hsl(210, 24%, 41%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 + hsl(210, 24%, 41%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill:#ffffff;" d="M17.822 23.297a6.644 6.644 0 00-.654.032c-1.104.1-2.451-.378-3.244-1.15a3.223 3.223 0 01-.52-.739c-.209-.425-.22-.489-.211-1.178a8.174 8.174 0 01.19-1.585c.243-1.151.155-1.449-.514-1.737-.4-.172-.632-.135-1 .16-.268.215-.28.463-.07 1.532.298 1.526.286 2.238-.05 2.907-.28.56-.443.703-1.287 1.133-1.005.513-1.461.638-2.332.638-.73 0-1.014-.082-1.276-.366-.134-.145-.148-.2-.085-.32.099-.184.329-.3.959-.488.277-.082.604-.236.727-.341.123-.105.329-.265.457-.354.32-.222.562-.761.563-1.254 0-.331-.188-1.034-.45-1.676-.138-.338-.38.085-.38.666 0 .434-.673 1.569-.93 1.569-.048 0-.288.101-.532.225-.43.219-.47.225-1.31.225-.815 0-.889-.011-1.235-.194-.42-.22-.902-.694-1.094-1.073a2.752 2.752 0 00-.227-.377c-.083-.102-.08-.143.018-.293.206-.314.473-.317 1.186-.011.583.25 1.22.215 1.582-.086.168-.139.325-.697.342-1.217.02-.598-.049-.66-.596-.528-.86.206-1.762-.084-2.76-.887-.916-.739-1.362-.845-2.241-.538-.262.092-.51.153-.552.137-.042-.016-.134-.136-.204-.268-.118-.218-.12-.252-.02-.403.156-.24.714-.573 1.185-.708.297-.086.588-.11 1.076-.09.655.026.687.035 1.567.458.54.259.99.43 1.127.43.27 0 1.014-.37 1.159-.577.167-.238.124-.34-.322-.776-1.19-1.16-1.943-2.608-2.24-4.31-.124-.702-.14-1.888-.035-2.483.116-.656.677-2.273.915-2.64.385-.59 1.823-1.965 2.585-2.469C9.187.905 11.43.395 13.715.785c2.457.42 4.507 1.61 5.849 3.394 1.062 1.414 1.554 2.859 1.553 4.57 0 1.778-.497 3.238-1.599 4.693a6.207 6.207 0 00-.34.476c0 .013.205.12.456.238.737.345 1.169.844 1.726 1.994.256.527.531 1.031.613 1.12.225.247.614.42 1.099.49.588.085.804.178.9.388.109.24-.111.55-.402.563-.11.005-.394.033-.63.062-.887.107-1.851-.251-2.416-.898-.17-.193-.503-.616-.74-.939-.455-.616-.818-.922-1.054-.888-.117.017-.14.066-.127.28.008.142.068.34.133.438.09.137.127.412.161 1.196.05 1.153.147 1.458.55 1.726.306.204.552.198 1.11-.025.581-.233.923-.238 1.159-.018.243.227.2.637-.11 1.026-.33.419-1.338.899-2.001.954-1.194.1-2.371-.602-2.828-1.686-.062-.147-.197-.61-.301-1.03-.12-.486-.221-.762-.28-.762-.109 0-.263.401-.27.705-.003.12-.056.417-.118.657-.328 1.282.307 2.309 1.66 2.684.657.182.808.299.808.623 0 .319-.165.494-.454.481z"/></svg>' + )); + } + } + + &--linkedin { + background: hsl(210, 90%, 40%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(210, 90%, 30%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 + hsl(210, 90%, 30%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 + hsl(210, 90%, 30%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path style="fill:#ffffff;" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>' + )); + } + } + + &--twitter { + background: hsl(203, 89%, 53%); + box-shadow: #{fun.convert-px(3)} #{fun.convert-px(3)} 0 0 hsl(203, 89%, 43%); + + &:hover, + &:focus { + box-shadow: #{fun.convert-px(6)} #{fun.convert-px(6)} 0 0 + hsl(203, 89%, 43%); + } + + &:active { + box-shadow: #{fun.convert-px(1)} #{fun.convert-px(1)} 0 0 + hsl(203, 89%, 43%); + } + + &::before { + background-image: url(fun.encode-svg( + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill:#ffffff;" d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>' + )); + } + } +} diff --git a/src/components/Widgets/Sharing/Sharing.tsx b/src/components/Widgets/Sharing/Sharing.tsx new file mode 100644 index 0000000..bc52f9b --- /dev/null +++ b/src/components/Widgets/Sharing/Sharing.tsx @@ -0,0 +1,113 @@ +import { ExpandableWidget } from '@components/WidgetParts'; +import sharingMedia from '@config/sharing'; +import { t } from '@lingui/macro'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import styles from './Sharing.module.scss'; + +type Parameters = { + content: string; + image: string; + title: string; + url: string; +}; + +type Website = { + id: string; + name: string; + parameters: Parameters; + url: string; +}; + +const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { + const [pageExcerpt, setPageExcerpt] = useState(''); + const [pageUrl, setPageUrl] = useState(''); + const [domainName, setDomainName] = useState(''); + const router = useRouter(); + + useEffect(() => { + const divEl = document.createElement('div'); + divEl.innerHTML = excerpt; + const cleanExcerpt = divEl.textContent!; + setPageExcerpt(cleanExcerpt); + }, [excerpt]); + + useEffect(() => { + const { protocol, hostname, port } = window.location; + const currentPort = port ? `:${port}` : ''; + const fullUrl = `${protocol}//${hostname}${currentPort}${router.asPath}`; + + setDomainName(hostname); + setPageUrl(fullUrl); + }, [router.asPath]); + + const getSharingUrl = (website: Website): string => { + const { id, parameters, url } = website; + let sharingUrl = `${url}?`; + let count = 0; + + for (const [key, value] of Object.entries(parameters)) { + if (!value) continue; + + sharingUrl += count > 0 ? `&${value}=` : `${value}=`; + + switch (key) { + case 'content': + if (id === 'email') { + const intro = t`Introduction:`; + const readMore = t`Read more here:`; + const body = `${intro}\n\n"${pageExcerpt}"\n\n${readMore} ${pageUrl}`; + sharingUrl += encodeURI(body); + } else { + sharingUrl += encodeURI(pageExcerpt); + } + break; + case 'title': + const prefix = id === 'email' ? t`Seen on ${domainName}:` : ''; + sharingUrl += encodeURI(`${prefix} ${title}`); + break; + case 'url': + sharingUrl += encodeURI(pageUrl); + break; + default: + break; + } + + count++; + } + + return sharingUrl; + }; + + const getItems = () => { + const websites: Website[] = sharingMedia; + + return websites.map((website) => { + const { id, name } = website; + const sharingUrl = getSharingUrl(website); + const linkModifier = `link--${id}`; + + return ( + <li key={id}> + <a + href={sharingUrl} + title={name} + className={`${styles.link} ${styles[linkModifier]}`} + > + <span className="screen-reader-text">{name}</span> + </a> + </li> + ); + }); + }; + + return ( + <ExpandableWidget title={t`Share`} expand={true}> + <ul className={`${styles.list} ${styles['list--sharing']}`}> + {getItems()} + </ul> + </ExpandableWidget> + ); +}; + +export default Sharing; diff --git a/src/components/Widgets/SocialMedia/SocialMedia.module.scss b/src/components/Widgets/SocialMedia/SocialMedia.module.scss new file mode 100644 index 0000000..c8ad759 --- /dev/null +++ b/src/components/Widgets/SocialMedia/SocialMedia.module.scss @@ -0,0 +1,48 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.list { + @extend %flex-list; + + gap: var(--spacing-xs); + align-items: center; + padding: var(--spacing-2xs) 0 0 var(--spacing-2xs); +} + +.link { + display: block; + width: 3em; + height: 3em; + background: none; + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-1) + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(4) fun.convert-px(-3) + var(--color-shadow-light), + 0 0 0 0 var(--color-shadow); + transition: all 0.3s linear 0s; + + &:hover, + &:focus { + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(1) fun.convert-px(2) fun.convert-px(-1) + var(--color-shadow-lighter), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) fun.convert-px(-4) + var(--color-shadow-lighter), + fun.convert-px(6) fun.convert-px(6) fun.convert-px(10) fun.convert-px(-3) + var(--color-shadow-light); + transform: scale(1.15); + } + + &:focus { + outline: var(--color-primary) dashed fun.convert-px(2); + } + + &:active { + box-shadow: 0 0 0 0 var(--color-shadow-light); + outline: none; + transform: scale(0.9); + } +} diff --git a/src/components/Widgets/SocialMedia/SocialMedia.tsx b/src/components/Widgets/SocialMedia/SocialMedia.tsx new file mode 100644 index 0000000..351fd48 --- /dev/null +++ b/src/components/Widgets/SocialMedia/SocialMedia.tsx @@ -0,0 +1,72 @@ +import { socialWebsites } from '@config/social-media'; +import GithubIcon from '@assets/images/social-media/github.svg'; +import GitlabIcon from '@assets/images/social-media/gitlab.svg'; +import LinkedInIcon from '@assets/images/social-media/linkedin.svg'; +import TwitterIcon from '@assets/images/social-media/twitter.svg'; +import styles from './SocialMedia.module.scss'; +import { ExpandableWidget } from '@components/WidgetParts'; + +const SocialMedia = ({ + title, + github = false, + gitlab = false, + linkedin = false, + twitter = false, +}: { + title: string; + github?: boolean; + gitlab?: boolean; + linkedin?: boolean; + twitter?: boolean; +}) => { + const getIcon = (id: string) => { + switch (id) { + case 'github': + return <GithubIcon />; + case 'gitlab': + return <GitlabIcon />; + case 'linkedin': + return <LinkedInIcon />; + case 'twitter': + return <TwitterIcon />; + default: + break; + } + }; + + const shouldDisplayLink = (id: string) => { + switch (id) { + case 'github': + return github; + case 'gitlab': + return gitlab; + case 'linkedin': + return linkedin; + case 'twitter': + return twitter; + default: + break; + } + }; + + const items = socialWebsites.map((website) => { + return shouldDisplayLink(website.id) ? ( + <li key={website.id}> + <a href={website.url} className={styles.link}> + {getIcon(website.id)} + <span className="screen-reader-text">{website.name}</span> + </a> + </li> + ) : ( + '' + ); + }); + + return ( + <ExpandableWidget title={title} expand={true}> + <ul className={styles.list}>{items}</ul> + </ExpandableWidget> + ); +}; + +export default SocialMedia; diff --git a/src/components/Widgets/ThematicsList/ThematicsList.tsx b/src/components/Widgets/ThematicsList/ThematicsList.tsx new file mode 100644 index 0000000..8523bd1 --- /dev/null +++ b/src/components/Widgets/ThematicsList/ThematicsList.tsx @@ -0,0 +1,46 @@ +import { ExpandableWidget, List } from '@components/WidgetParts'; +import { t } from '@lingui/macro'; +import { getAllThematics } from '@services/graphql/queries'; +import { TitleLevel } from '@ts/types/app'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import useSWR from 'swr'; + +const ThematicsList = ({ + title, + titleLevel, +}: { + title: string; + titleLevel?: TitleLevel; +}) => { + const router = useRouter(); + const isThematic = () => router.asPath.includes('/thematique/'); + const currentThematicSlug = isThematic() + ? router.asPath.replace('/thematique/', '') + : ''; + + const { data, error } = useSWR('/api/thematics', getAllThematics); + + if (error) return <div>{t`Failed to load.`}</div>; + if (!data) return <div>{t`Loading...`}</div>; + + const thematics = data.map((thematic) => { + return currentThematicSlug !== thematic.slug ? ( + <li key={thematic.databaseId}> + <Link href={`/thematique/${thematic.slug}`}> + <a>{thematic.title}</a> + </Link> + </li> + ) : ( + '' + ); + }); + + return ( + <ExpandableWidget title={title} titleLevel={titleLevel} withBorders={true}> + <List items={thematics} /> + </ExpandableWidget> + ); +}; + +export default ThematicsList; diff --git a/src/components/Widgets/ToC/ToC.module.scss b/src/components/Widgets/ToC/ToC.module.scss new file mode 100644 index 0000000..a296659 --- /dev/null +++ b/src/components/Widgets/ToC/ToC.module.scss @@ -0,0 +1,70 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.item { + width: 100%; + margin: 0; + + &::before { + display: none; + } +} + +.link { + display: flex; + flex-flow: row nowrap; + width: 100%; + padding: var(--spacing-2xs) var(--spacing-sm); + background: none; + border-bottom: fun.convert-px(1) solid var(--color-border-lighter); + text-decoration: underline solid transparent 0; + transition: all 0.16s ease-in-out 0s, text-decoration-color 0s; + + &:hover, + &:focus { + background: var(--color-bg-secondary); + } + + &:focus { + color: var(--color-primary); + text-decoration-color: var(--color-primary-light); + text-decoration-thickness: 0.25ex; + } + + &:active { + background: var(--color-bg-tertiary); + text-decoration-color: transparent; + text-decoration-thickness: 0; + } +} + +.list { + width: 100%; + margin: 0; + counter-reset: link; + + @include mix.media("screen") { + @include mix.dimensions("lg") { + font-size: var(--font-size-sm); + font-weight: 500; + } + } + + > .item .link { + counter-increment: link; + + &::before { + content: counters(link, ".") ". "; + color: var(--color-secondary); + padding-right: var(--spacing-2xs); + } + } + + .item .item .link::before { + padding-left: var(--spacing-sm); + } + + .item .item .item .link::before { + padding-left: var(--spacing-lg); + } +} diff --git a/src/components/Widgets/ToC/ToC.tsx b/src/components/Widgets/ToC/ToC.tsx new file mode 100644 index 0000000..6010354 --- /dev/null +++ b/src/components/Widgets/ToC/ToC.tsx @@ -0,0 +1,30 @@ +import { ExpandableWidget, OrderedList } from '@components/WidgetParts'; +import { t } from '@lingui/macro'; +import { Heading } from '@ts/types/app'; +import useHeadingsTree from '@utils/hooks/useHeadingsTree'; + +const ToC = () => { + const headingsTree = useHeadingsTree('article'); + const title = t`Table of contents`; + + const getItems = (headings: Heading[]) => { + return headings.map((heading) => { + return ( + <li key={heading.id}> + <a href={`#${heading.id}`}>{heading.title}</a> + {heading.children.length > 0 && ( + <OrderedList items={getItems(heading.children)} /> + )} + </li> + ); + }); + }; + + return ( + <ExpandableWidget title={title} expand={true} withBorders={true}> + <OrderedList items={getItems(headingsTree)} /> + </ExpandableWidget> + ); +}; + +export default ToC; diff --git a/src/components/Widgets/TopicsList/TopicsList.tsx b/src/components/Widgets/TopicsList/TopicsList.tsx new file mode 100644 index 0000000..5bf12b9 --- /dev/null +++ b/src/components/Widgets/TopicsList/TopicsList.tsx @@ -0,0 +1,46 @@ +import { ExpandableWidget, List } from '@components/WidgetParts'; +import { t } from '@lingui/macro'; +import { getAllSubjects } from '@services/graphql/queries'; +import { TitleLevel } from '@ts/types/app'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import useSWR from 'swr'; + +const TopicsList = ({ + title, + titleLevel, +}: { + title: string; + titleLevel?: TitleLevel; +}) => { + const router = useRouter(); + const isTopic = () => router.asPath.includes('/sujet/'); + const currentTopicSlug = isTopic() + ? router.asPath.replace('/sujet/', '') + : ''; + + const { data, error } = useSWR('/api/subjects', getAllSubjects); + + if (error) return <div>{t`Failed to load.`}</div>; + if (!data) return <div>{t`Loading...`}</div>; + + const subjects = data.map((subject) => { + return currentTopicSlug !== subject.slug ? ( + <li key={subject.databaseId}> + <Link href={`/sujet/${subject.slug}`}> + <a>{subject.title}</a> + </Link> + </li> + ) : ( + '' + ); + }); + + return ( + <ExpandableWidget title={title} titleLevel={titleLevel} withBorders={true}> + <List items={subjects} /> + </ExpandableWidget> + ); +}; + +export default TopicsList; diff --git a/src/components/Widgets/index.tsx b/src/components/Widgets/index.tsx new file mode 100644 index 0000000..8354449 --- /dev/null +++ b/src/components/Widgets/index.tsx @@ -0,0 +1,21 @@ +import CVPreview from './CVPreview/CVPreview'; +import RecentPosts from './RecentPosts/RecentPosts'; +import RelatedThematics from './RelatedThematics/RelatedThematics'; +import RelatedTopics from './RelatedTopics/RelatedTopics'; +import Sharing from './Sharing/Sharing'; +import SocialMedia from './SocialMedia/SocialMedia'; +import ThematicsList from './ThematicsList/ThematicsList'; +import ToC from './ToC/ToC'; +import TopicsList from './TopicsList/TopicsList'; + +export { + CVPreview, + RecentPosts, + RelatedThematics, + RelatedTopics, + Sharing, + SocialMedia, + ThematicsList, + ToC, + TopicsList, +}; |
