mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-22 09:15:33 +03:00
Trending news carousel
This commit is contained in:
parent
e7ef20f265
commit
1c5453cfb6
2 changed files with 362 additions and 19 deletions
184
src/pages/trending.css
Normal file
184
src/pages/trending.css
Normal file
|
@ -0,0 +1,184 @@
|
|||
.links-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px 16px 20px calc(16px + 1em + 12px);
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
background-color: var(--bg-faded-color);
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black 16px,
|
||||
black calc(100% - 16px),
|
||||
transparent
|
||||
);
|
||||
text-shadow: 0 1px var(--bg-blur-color);
|
||||
transition: opacity 0.3s ease-out;
|
||||
|
||||
&:not(#columns &) {
|
||||
@media (min-width: 40em) {
|
||||
width: 95vw;
|
||||
max-width: calc(320px * 3.3);
|
||||
transform: translateX(calc(-50% + var(--main-width) / 2));
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 90%;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-insignificant-color);
|
||||
position: absolute;
|
||||
top: calc(16px + 8px);
|
||||
left: 16px;
|
||||
transform-origin: top left;
|
||||
transform: rotate(-90deg) translateX(-100%);
|
||||
user-select: none;
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
var(--text-color),
|
||||
var(--link-color)
|
||||
);
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
--other-color: var(--light-color);
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--other-color: var(--dark-color);
|
||||
}
|
||||
min-width: 240px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background-color: var(--other-color);
|
||||
border: 4px solid transparent;
|
||||
box-shadow: 0 4px 8px -2px var(--drop-shadow-color);
|
||||
transition: all 0.15s ease-out;
|
||||
display: flex;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--average-color) -50%,
|
||||
transparent
|
||||
);
|
||||
background-clip: border-box;
|
||||
background-origin: border-box;
|
||||
height: 320px;
|
||||
|
||||
&:not(:active):is(:hover, :focus-visible) {
|
||||
border-color: var(--average-color);
|
||||
box-shadow: 0 4px 8px var(--drop-shadow-color),
|
||||
0 8px 16px var(--drop-shadow-color);
|
||||
transform-origin: center bottom;
|
||||
transform: scale(1.02);
|
||||
|
||||
img {
|
||||
animation: position-object 5s ease-in-out 1s 5;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition: none;
|
||||
transform: scale(1.015);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background-color: var(--bg-color);
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--other-color) 70%,
|
||||
var(--bg-color) 100%
|
||||
);
|
||||
transition: background-position-y 0.15s ease-out;
|
||||
|
||||
&:is(:hover, :focus-visible) {
|
||||
background-position-y: -40px;
|
||||
}
|
||||
|
||||
figure {
|
||||
flex-grow: 1;
|
||||
margin: 0 0 -16px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
vertical-align: top;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
hsl(0, 0%, 0%) 0%,
|
||||
hsla(0, 0%, 0%, 0.987) 14%,
|
||||
hsla(0, 0%, 0%, 0.951) 26.2%,
|
||||
hsla(0, 0%, 0%, 0.896) 36.8%,
|
||||
hsla(0, 0%, 0%, 0.825) 45.9%,
|
||||
hsla(0, 0%, 0%, 0.741) 53.7%,
|
||||
hsla(0, 0%, 0%, 0.648) 60.4%,
|
||||
hsla(0, 0%, 0%, 0.55) 66.2%,
|
||||
hsla(0, 0%, 0%, 0.45) 71.2%,
|
||||
hsla(0, 0%, 0%, 0.352) 75.6%,
|
||||
hsla(0, 0%, 0%, 0.259) 79.6%,
|
||||
hsla(0, 0%, 0%, 0.175) 83.4%,
|
||||
hsla(0, 0%, 0%, 0.104) 87.2%,
|
||||
hsla(0, 0%, 0%, 0.049) 91.1%,
|
||||
hsla(0, 0%, 0%, 0.013) 95.3%,
|
||||
hsla(0, 0%, 0%, 0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.article-body {
|
||||
padding: 0 8px 8px;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
color: var(--text-insignificant-color);
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover .domain {
|
||||
color: var(--link-text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-wrap: balance;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-insignificant-color);
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import './trending.css';
|
||||
|
||||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
@ -6,15 +9,27 @@ import { useSnapshot } from 'valtio';
|
|||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Menu2 from '../components/menu2';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
import pmem from '../utils/pmem';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
const fetchLinks = pmem(
|
||||
(masto) => {
|
||||
return masto.v1.trends.links.list().next();
|
||||
},
|
||||
{
|
||||
// News last much longer
|
||||
maxAge: 10 * 60 * 1000, // 10 minutes
|
||||
},
|
||||
);
|
||||
|
||||
function Trending({ columnMode, ...props }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const params = columnMode ? {} : useParams();
|
||||
|
@ -27,6 +42,7 @@ function Trending({ columnMode, ...props }) {
|
|||
const latestItem = useRef();
|
||||
|
||||
const [hashtags, setHashtags] = useState([]);
|
||||
const [links, setLinks] = useState([]);
|
||||
const trendIterator = useRef();
|
||||
async function fetchTrend(firstLoad) {
|
||||
if (firstLoad || !trendIterator.current) {
|
||||
|
@ -38,11 +54,20 @@ function Trending({ columnMode, ...props }) {
|
|||
try {
|
||||
const iterator = masto.v1.trends.tags.list();
|
||||
const { value: tags } = await iterator.next();
|
||||
console.log(tags);
|
||||
console.log('tags', tags);
|
||||
setHashtags(tags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Get links
|
||||
try {
|
||||
const { value: links } = await fetchLinks(masto);
|
||||
console.log('links', links);
|
||||
setLinks(links);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const results = await trendIterator.current.next();
|
||||
let { value } = results;
|
||||
|
@ -84,26 +109,125 @@ function Trending({ columnMode, ...props }) {
|
|||
}
|
||||
|
||||
const TimelineStart = useMemo(() => {
|
||||
if (!hashtags.length) return null;
|
||||
return (
|
||||
<div class="filter-bar">
|
||||
<Icon icon="chart" class="insignificant" size="l" />
|
||||
{hashtags.map((tag, i) => {
|
||||
const { name, history } = tag;
|
||||
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
|
||||
return (
|
||||
<Link to={`/${instance}/t/${name}`}>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{name}
|
||||
</span>
|
||||
<span class="filter-count">{total.toLocaleString()}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<>
|
||||
{!!hashtags.length && (
|
||||
<div class="filter-bar">
|
||||
<Icon icon="chart" class="insignificant" size="l" />
|
||||
{hashtags.map((tag, i) => {
|
||||
const { name, history } = tag;
|
||||
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
|
||||
return (
|
||||
<Link to={`/${instance}/t/${name}`} key={name}>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{name}
|
||||
</span>
|
||||
<span class="filter-count">{total.toLocaleString()}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!!links.length && (
|
||||
<div class="links-bar">
|
||||
<h3>Trending News</h3>
|
||||
{links.map((link) => {
|
||||
const {
|
||||
authorName,
|
||||
authorUrl,
|
||||
blurhash,
|
||||
description,
|
||||
height,
|
||||
image,
|
||||
imageDescription,
|
||||
language,
|
||||
providerName,
|
||||
providerUrl,
|
||||
publishedAt,
|
||||
title,
|
||||
url,
|
||||
width,
|
||||
} = link;
|
||||
const domain = new URL(url).hostname
|
||||
.replace(/^www\./, '')
|
||||
.replace(/\/$/, '');
|
||||
const averageColor = getBlurHashAverageColor(blurhash);
|
||||
const labAverageColor = rgb2oklab(averageColor);
|
||||
|
||||
// const lightColor = averageColor.map((c) => {
|
||||
// const v = c + 120;
|
||||
// return v > 255 ? 255 : v;
|
||||
// });
|
||||
// const darkColor = averageColor.map((c) => {
|
||||
// const v = c - 100;
|
||||
// return v < 0 ? 0 : v;
|
||||
// });
|
||||
const lightColor = labAverageColor.map((c, i) => {
|
||||
if (i === 0) {
|
||||
return 0.9;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
const darkColor = labAverageColor.map((c, i) => {
|
||||
if (i === 0) {
|
||||
return 0.4;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
return (
|
||||
<a
|
||||
key={url}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
'--average-color': `rgb(${averageColor?.join(',')})`,
|
||||
// '--light-color': `rgb(${lightColor?.join(',')})`,
|
||||
// '--dark-color': `rgb(${darkColor?.join(',')})`,
|
||||
'--light-color': `oklab(${lightColor?.join(' ')})`,
|
||||
'--dark-color': `oklab(${darkColor?.join(' ')})`,
|
||||
}}
|
||||
>
|
||||
<article>
|
||||
<figure>
|
||||
<img
|
||||
src={image}
|
||||
alt={imageDescription}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</figure>
|
||||
<div class="article-body">
|
||||
<header>
|
||||
<div class="article-meta">
|
||||
<span class="domain">{domain}</span>{' '}
|
||||
{!!publishedAt && <>· </>}
|
||||
{!!publishedAt && (
|
||||
<>
|
||||
<RelativeTime
|
||||
datetime={publishedAt}
|
||||
format="micro"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!!title && <h1 class="title">{title}</h1>}
|
||||
</header>
|
||||
{!!description && (
|
||||
<p class="description">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [hashtags]);
|
||||
}, [hashtags, links]);
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
|
@ -164,4 +288,39 @@ function Trending({ columnMode, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function rgb2oklab(rgb) {
|
||||
// Normalize RGB values to the range [0, 1]
|
||||
const r = rgb[0] / 255;
|
||||
const g = rgb[1] / 255;
|
||||
const b = rgb[2] / 255;
|
||||
|
||||
// Linearize RGB values
|
||||
const rLinear = r <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
|
||||
const gLinear = g <= 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
|
||||
const bLinear = b <= 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
|
||||
|
||||
// Convert to XYZ color space
|
||||
const x = rLinear * 0.4124564 + gLinear * 0.3575761 + bLinear * 0.1804375;
|
||||
const y = rLinear * 0.2126729 + gLinear * 0.7151522 + bLinear * 0.072175;
|
||||
const z = rLinear * 0.0193339 + gLinear * 0.119192 + bLinear * 0.9503041;
|
||||
|
||||
// Normalize to reference white
|
||||
const xNormalized = x / 0.95047;
|
||||
const yNormalized = y / 1.0;
|
||||
const zNormalized = z / 1.08883;
|
||||
|
||||
// Non-linear transfer function for luminance
|
||||
const fy =
|
||||
yNormalized > 0.008856
|
||||
? Math.cbrt(yNormalized)
|
||||
: (903.3 * yNormalized + 16.0) / 116.0;
|
||||
|
||||
// Calculate OkLab values
|
||||
const l = Math.max(0, Math.min(1, (116.0 * fy - 16.0) / 100));
|
||||
const a = (xNormalized - yNormalized) * 0.21;
|
||||
const bValue = (yNormalized - zNormalized) * 0.12;
|
||||
|
||||
return [l, a, bValue];
|
||||
}
|
||||
|
||||
export default Trending;
|
||||
|
|
Loading…
Reference in a new issue