From 2b4173d7598c8e4b240ae365c3f5f46d24f9eaca Mon Sep 17 00:00:00 2001 From: Max Richter Date: Sat, 12 Aug 2023 18:32:56 +0200 Subject: [PATCH] feat: make author clickable --- components/Card.tsx | 46 +++++++++++++----- components/Image.tsx | 9 ++-- components/MovieCard.tsx | 23 --------- components/RecipeCard.tsx | 20 -------- components/RecipeHero.tsx | 21 ++++++--- lib/cache/image.ts | 95 +++++++++++++++++++++++++------------- lib/crud.ts | 3 +- lib/promise.ts | 23 +++++++++ lib/resource/articles.ts | 18 ++++---- lib/resource/movies.ts | 7 ++- lib/resource/recipes.ts | 1 + lib/resource/series.ts | 22 ++------- lib/search.ts | 5 +- lib/string.ts | 12 +++++ lib/thumbhash.ts | 3 +- lib/types.ts | 12 +++++ routes/_app.tsx | 9 ++++ routes/api/images/index.ts | 13 +++--- routes/articles/index.tsx | 21 ++++----- routes/index.tsx | 5 +- routes/movies/[name].tsx | 8 +++- routes/movies/index.tsx | 8 ++-- routes/recipes/index.tsx | 8 ++-- routes/series/[name].tsx | 2 +- routes/series/index.tsx | 8 ++-- static/global.css | 21 +++++---- static/thumbnails.js | 8 +++- 27 files changed, 257 insertions(+), 174 deletions(-) delete mode 100644 components/MovieCard.tsx delete mode 100644 components/RecipeCard.tsx diff --git a/components/Card.tsx b/components/Card.tsx index 19d44a2..f313a5e 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -1,9 +1,11 @@ -import { isYoutubeLink } from "@lib/string.ts"; +import { isLocalImage, isYoutubeLink } from "@lib/string.ts"; import { IconBrandYoutube } from "@components/icons.tsx"; +import { GenericResource } from "@lib/types.ts"; export function Card( - { link, title, image, thumbnail, backgroundSize = 100 }: { + { link, title, image, thumbnail, backgroundColor, backgroundSize = 100 }: { backgroundSize?: number; + backgroundColor?: string; thumbnail?: string; link?: string; title?: string; @@ -12,7 +14,7 @@ export function Card( ) { const backgroundStyle = { backgroundSize: "cover", - boxShadow: "0px -60px 90px black inset, 0px 10px 20px #fff1 inset", + backgroundColor: backgroundColor, }; if (backgroundSize !== 100) { @@ -26,18 +28,20 @@ export function Card( href={link} style={backgroundStyle} data-thumb={thumbnail} - class="text-white rounded-3xl shadow-md relative + class="text-white rounded-3xl shadow-md relative lg:w-56 lg:h-56 sm:w-48 sm:h-48 w-[37vw] h-[37vw]" > {true && ( - + + + )}
@@ -63,3 +67,23 @@ export function Card( ); } + +export function ResourceCard( + { res, sublink = "movies" }: { sublink?: string; res: GenericResource }, +) { + const { meta: { image = "/placeholder.svg" } = {} } = res; + + const imageUrl = isLocalImage(image) + ? `/api/images?image=${image}&width=200&height=200` + : image; + + return ( + + ); +} diff --git a/components/Image.tsx b/components/Image.tsx index b6d4782..9cb13c2 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -36,6 +36,7 @@ const Image = ( src: string; alt?: string; thumbnail?: string; + fill?: boolean; width?: number | string; height?: number | string; style?: CSS.HtmlAttributes; @@ -48,10 +49,10 @@ const Image = ( return ( diff --git a/components/MovieCard.tsx b/components/MovieCard.tsx deleted file mode 100644 index e96c467..0000000 --- a/components/MovieCard.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Card } from "@components/Card.tsx"; -import { Movie } from "@lib/resource/movies.ts"; -import { Series } from "@lib/resource/series.ts"; -import { isLocalImage } from "@lib/string.ts"; - -export function MovieCard( - { movie, sublink = "movies" }: { sublink?: string; movie: Movie | Series }, -) { - const { meta: { image = "/placeholder.svg" } = {} } = movie; - - const imageUrl = isLocalImage(image) - ? `/api/images?image=${image}&width=200&height=200` - : image; - - return ( - - ); -} diff --git a/components/RecipeCard.tsx b/components/RecipeCard.tsx deleted file mode 100644 index 1ff0e89..0000000 --- a/components/RecipeCard.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Card } from "@components/Card.tsx"; -import { Recipe } from "@lib/resource/recipes.ts"; -import { isLocalImage } from "@lib/string.ts"; - -export function RecipeCard({ recipe }: { recipe: Recipe }) { - const { meta: { image = "/placeholder.svg" } = {} } = recipe; - - const imageUrl = isLocalImage(image) - ? `/api/images?image=${image}&width=200&height=200` - : image; - - return ( - - ); -} diff --git a/components/RecipeHero.tsx b/components/RecipeHero.tsx index ea8f288..bb31e01 100644 --- a/components/RecipeHero.tsx +++ b/components/RecipeHero.tsx @@ -5,20 +5,20 @@ import { IconExternalLink, } from "@components/icons.tsx"; import Image from "@components/Image.tsx"; +import { GenericResource } from "@lib/types.ts"; export function RecipeHero( { data, subline, backlink, editLink }: { backlink: string; - subline?: string[]; + subline?: (string | { title: string; href: string })[]; editLink?: string; - data: { - meta?: { thumbnail?: string; image?: string; link?: string }; - name: string; - }; + data: GenericResource; }, ) { const { meta: { image } = {} } = data; + console.log({ meta: data.meta }); + return (
- {subline.filter((s) => s && s?.length > 1).map((s) => { - return {s}; + {subline.filter((s) => + s && (typeof s === "string" ? s?.length > 1 : true) + ).map((s) => { + if (typeof s === "string") { + return {s}; + } else { + return {s.title}; + } })}
)} diff --git a/lib/cache/image.ts b/lib/cache/image.ts index 2f92b3b..70176b9 100644 --- a/lib/cache/image.ts +++ b/lib/cache/image.ts @@ -1,4 +1,4 @@ -import { hash, isLocalImage } from "@lib/string.ts"; +import { hash, isLocalImage, rgbToHex } from "@lib/string.ts"; import * as cache from "@lib/cache/cache.ts"; import { ImageMagick, @@ -8,32 +8,45 @@ import { createLogger } from "@lib/log.ts"; import { generateThumbhash } from "@lib/thumbhash.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; -type ImageCacheOptions = { +type ImageCacheOptionsBasic = { url: string; + mediaType?: string; +}; + +interface ImageCacheOptionsDimensions extends ImageCacheOptionsBasic { width: number; height: number; - mediaType?: string; - suffix?: string; -}; +} + +interface ImageCacheOptionsSuffix extends ImageCacheOptionsBasic { + suffix: string; +} + +type ImageCacheOptions = ImageCacheOptionsDimensions | ImageCacheOptionsSuffix; const CACHE_KEY = "images"; const log = createLogger("cache/image"); function getCacheKey( - { url: _url, width, height, suffix }: ImageCacheOptions, + opts: ImageCacheOptions, ) { - const isLocal = isLocalImage(_url); - const url = new URL(isLocal ? `${SILVERBULLET_SERVER}/${_url}` : _url); + const isLocal = isLocalImage(opts.url); + const url = new URL( + isLocal ? `${SILVERBULLET_SERVER}/${opts.url}` : opts.url, + ); - const _suffix = suffix || `${width}:${height}`; + const _suffix = "suffix" in opts + ? opts.suffix + : `${opts.width}:${opts.height}`; - return `${CACHE_KEY}:${url.hostname}:${ + const cacheId = `${CACHE_KEY}:${url.hostname}:${ url.pathname.replaceAll("/", ":") }:${_suffix}` .replace( "::", ":", ); + return cacheId; } export function createThumbhash( @@ -53,7 +66,21 @@ export function createThumbhash( "RGBA", ); if (!bytes) return; - const hash = generateThumbhash(bytes, _image.width, _image.height); + const [hash, average] = generateThumbhash( + bytes, + _image.width, + _image.height, + ); + + if (average) { + cache.set( + getCacheKey({ + url, + suffix: "average", + }), + rgbToHex(average.r, average.g, average.b), + ); + } if (hash) { const b64 = btoa(String.fromCharCode(...hash)); @@ -61,8 +88,6 @@ export function createThumbhash( getCacheKey({ url, suffix: "thumbnail", - width: _image.width, - height: _image.height, }), b64, ); @@ -91,18 +116,26 @@ function verifyImage( } export function getThumbhash({ url }: { url: string }) { - return cache.get( - getCacheKey({ - url, - suffix: "thumbnail", - width: 200, - height: 200, - }), + return Promise.all( + [ + cache.get( + getCacheKey({ + url, + suffix: "thumbnail", + }), + ), + cache.get( + getCacheKey({ + url, + suffix: "average", + }), + ), + ] as const, ); } -export async function getImage({ url, width, height }: ImageCacheOptions) { - const cacheKey = getCacheKey({ url, width, height }); +export async function getImage(opts: ImageCacheOptions) { + const cacheKey = getCacheKey(opts); const pointerCacheRaw = await cache.get(cacheKey); if (!pointerCacheRaw) return; @@ -122,31 +155,29 @@ export async function getImage({ url, width, height }: ImageCacheOptions) { export async function setImage( buffer: Uint8Array, - { url, width, height, mediaType }: ImageCacheOptions, + opts: ImageCacheOptions, ) { const clone = new Uint8Array(buffer); const imageCorrect = await verifyImage(clone); if (!imageCorrect) { - log.info("failed to store image", { url }); + log.info("failed to store image", { url: opts.url }); return; } - const cacheKey = getCacheKey({ url, width, height }); + const cacheKey = getCacheKey(opts); const pointerId = await hash(cacheKey); - await cache.set(`image:${pointerId}`, clone); - cache.expire(pointerId, 60 * 60 * 24); - cache.expire(cacheKey, 60 * 60 * 24); + await cache.set(`image:${pointerId}`, clone, { expires: 60 * 60 * 24 }); await cache.set( cacheKey, JSON.stringify({ id: pointerId, - url, - width, - height, - mediaType, + ...("suffix" in opts + ? { suffix: opts.suffix } + : { width: opts.width, height: opts.height }), }), + { expires: 60 * 60 * 24 }, ); } diff --git a/lib/crud.ts b/lib/crud.ts index 78d3eb5..d75ffb8 100644 --- a/lib/crud.ts +++ b/lib/crud.ts @@ -22,12 +22,13 @@ export async function addThumbnailToResource( ): Promise { const imageUrl = res?.meta?.image; if (!imageUrl) return res; - const thumbhash = await getThumbhash({ url: imageUrl }); + const [thumbhash, average] = await getThumbhash({ url: imageUrl }); if (!thumbhash) return res; return { ...res, meta: { ...res?.meta, + average: average, thumbnail: thumbhash, }, }; diff --git a/lib/promise.ts b/lib/promise.ts index b308c3d..b131cf9 100644 --- a/lib/promise.ts +++ b/lib/promise.ts @@ -80,3 +80,26 @@ export class PromiseQueue { return true; } } + +export class ConcurrentPromiseQueue { + /** + * Eingereihte Promises. + */ + private queues: PromiseQueue[] = []; + + constructor(concurrency: number) { + this.queues = Array.from({ length: concurrency }).map(() => { + return new PromiseQueue(); + }); + } + + private queueIndex = 0; + private getQueue() { + this.queueIndex = (this.queueIndex + 1) % this.queues.length; + return this.queues[this.queueIndex]; + } + + public enqueue(promise: () => Promise): Promise { + return this.getQueue().enqueue(promise); + } +} diff --git a/lib/resource/articles.ts b/lib/resource/articles.ts index 4bb8fed..70e2b0c 100644 --- a/lib/resource/articles.ts +++ b/lib/resource/articles.ts @@ -17,18 +17,13 @@ export type Article = { date: Date; link: string; thumbnail?: string; + average?: string; image?: string; author?: string; rating?: number; }; }; -const crud = createCrud
({ - prefix: "Media/articles/", - parse: parseArticle, - hasThumbnails: true, -}); - function renderArticle(article: Article) { const meta = article.meta; if ("date" in meta) { @@ -100,9 +95,12 @@ function parseArticle(original: string, id: string): Article { }; } +const crud = createCrud
({ + prefix: "Media/articles/", + parse: parseArticle, + render: renderArticle, + hasThumbnails: true, +}); export const getAllArticles = crud.readAll; export const getArticle = crud.read; -export const createArticle = (article: Article) => { - const content = renderArticle(article); - return crud.create(article.id, content); -}; +export const createArticle = crud.create; diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts index fec206b..996d6c4 100644 --- a/lib/resource/movies.ts +++ b/lib/resource/movies.ts @@ -14,6 +14,7 @@ export type Movie = { date: Date; image: string; thumbnail?: string; + average?: string; author: string; rating: number; status: "not-seen" | "watch-again" | "finished"; @@ -99,12 +100,10 @@ export function parseMovie(original: string, id: string): Movie { const crud = createCrud({ prefix: "Media/movies/", parse: parseMovie, + render: renderMovie, hasThumbnails: true, }); export const getMovie = crud.read; export const getAllMovies = crud.readAll; -export const createMovie = (movie: Movie) => { - const content = renderMovie(movie); - return crud.create(movie.id, content); -}; +export const createMovie = crud.create; diff --git a/lib/resource/recipes.ts b/lib/resource/recipes.ts index da37a30..4da3fe5 100644 --- a/lib/resource/recipes.ts +++ b/lib/resource/recipes.ts @@ -37,6 +37,7 @@ export type Recipe = { rating?: number; portion?: number; author?: string; + average?: string; thumbnail?: string; }; }; diff --git a/lib/resource/series.ts b/lib/resource/series.ts index 120beb5..575cda8 100644 --- a/lib/resource/series.ts +++ b/lib/resource/series.ts @@ -16,6 +16,7 @@ export type Series = { image: string; author: string; rating: number; + average?: string; thumbnail?: string; status: "not-seen" | "watch-again" | "finished"; }; @@ -100,25 +101,10 @@ export function parseSeries(original: string, id: string): Series { const crud = createCrud({ prefix: "Media/series/", parse: parseSeries, + render: renderSeries, hasThumbnails: true, }); -export const getSeries = (id: string) => - crud.read(id).then(async (serie) => { - const imageUrl = serie.meta?.image; - if (!imageUrl) return serie; - const thumbhash = await getThumbhash({ url: imageUrl }); - if (!thumbhash) return serie; - return { - ...serie, - meta: { - ...serie.meta, - thumbnail: btoa(String.fromCharCode(...thumbhash)), - }, - }; - }); +export const getSeries = crud.read; export const getAllSeries = crud.readAll; -export const createSeries = (series: Series) => { - const content = renderSeries(series); - return crud.create(series.id, content); -}; +export const createSeries = crud.create; diff --git a/lib/search.ts b/lib/search.ts index 58bca97..655c8f8 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -1,6 +1,6 @@ import { BadRequestError } from "@lib/errors.ts"; import { resources } from "@lib/resources.ts"; -import { ResourceStatus } from "@lib/types.ts"; +import { ResourceStatus, SearchResult } from "@lib/types.ts"; import { getTypeSenseClient } from "@lib/typesense.ts"; import { extractHashTags } from "@lib/string.ts"; @@ -11,6 +11,7 @@ type SearchParams = { type?: ResourceType; tags?: string[]; status?: ResourceStatus; + author?: string; query_by?: string; }; @@ -44,7 +45,7 @@ export function parseResourceUrl(_url: string): SearchParams | undefined { export async function searchResource( { q, query_by = "name,description,author,tags", tags = [], type, status }: SearchParams, -) { +): Promise { const typesenseClient = await getTypeSenseClient(); if (!typesenseClient) { throw new Error("Query not available"); diff --git a/lib/string.ts b/lib/string.ts index 51d6269..e9af1ad 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -104,3 +104,15 @@ export const isLocalImage = (src: string) => export const isString = (input: string | undefined): input is string => { return typeof input === "string"; }; + +function componentToHex(c: number) { + if (c <= 1) { + c = Math.round(c * 255); + } + const hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; +} + +export function rgbToHex(r: number, g: number, b: number) { + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); +} diff --git a/lib/thumbhash.ts b/lib/thumbhash.ts index 5b44d12..7f90862 100644 --- a/lib/thumbhash.ts +++ b/lib/thumbhash.ts @@ -1,7 +1,8 @@ import * as thumbhash from "https://esm.sh/thumbhash@0.1.1"; export function generateThumbhash(buffer: Uint8Array, w: number, h: number) { - return thumbhash.rgbaToThumbHash(w, h, buffer); + const hash = thumbhash.rgbaToThumbHash(w, h, buffer); + return [hash, thumbhash.thumbHashToAverageRGBA(hash)] as const; } export function generateDataURL(hash: string) { diff --git a/lib/types.ts b/lib/types.ts index c503388..fea5945 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -34,6 +34,18 @@ export interface TMDBSeries { vote_count: number; } +export type GenericResource = { + name: string; + id: string; + type: keyof typeof resources; + meta?: { + image?: string; + author?: string; + average?: string; + thumbnail?: string; + }; +}; + export interface GiteaOauthUser { sub: string; name: string; diff --git a/routes/_app.tsx b/routes/_app.tsx index 8e26663..98dde8f 100644 --- a/routes/_app.tsx +++ b/routes/_app.tsx @@ -37,6 +37,15 @@ export default function App({ Component }: AppProps) { --background: rgb(43, 41, 48); --foreground: rgb(129, 129, 129); } + .custom-grid { + grid-template-columns: repeat(auto-fit, minmax(37vw, 1fr)) ; + } + + @media(min-width: 640px){ + .custom-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) ; + } + } /* work-sans-regular - latin */ @font-face { font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ diff --git a/routes/api/images/index.ts b/routes/api/images/index.ts index 030a977..528f3ba 100644 --- a/routes/api/images/index.ts +++ b/routes/api/images/index.ts @@ -9,7 +9,7 @@ import { import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts"; import * as cache from "@lib/cache/image.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; -import { PromiseQueue } from "@lib/promise.ts"; +import { ConcurrentPromiseQueue, PromiseQueue } from "@lib/promise.ts"; import { BadRequestError } from "@lib/errors.ts"; import { createLogger } from "@lib/log.ts"; @@ -109,7 +109,7 @@ function parseParams(reqUrl: URL) { }; } -const queue = new PromiseQueue(); +const queue = new ConcurrentPromiseQueue(2); async function processImage(imageUrl: string, params: ImageParams) { const remoteImage = await getRemoteImage(imageUrl); @@ -165,20 +165,21 @@ const GET = async ( processImage(imageUrl, params) ); + const clonedImage = resizedImage.slice(); setTimeout(() => { - cache.setImage(resizedImage.slice(), { + cache.setImage(clonedImage, { url: imageUrl, width: params.width, height: params.height, mediaType: mediaType, }); - }, 10); + }, 50); log.debug("not-cached", { imageUrl }); - cache.getThumbhash({ url: imageUrl }).then((hash) => { + cache.getThumbhash({ url: imageUrl }).then(([hash]) => { if (!hash) { - cache.createThumbhash(resizedImage.slice(), imageUrl).catch((_err) => { + cache.createThumbhash(clonedImage.slice(), imageUrl).catch((_err) => { // }); } diff --git a/routes/articles/index.tsx b/routes/articles/index.tsx index 626eb94..7331fb1 100644 --- a/routes/articles/index.tsx +++ b/routes/articles/index.tsx @@ -8,8 +8,11 @@ import { IconArrowLeft } from "@components/icons.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { SearchResult } from "@lib/types.ts"; +import { ResourceCard } from "@components/Card.tsx"; -export const handler: Handlers = { +export const handler: Handlers< + { articles: Article[] | null; searchResults?: SearchResult } +> = { async GET(req, ctx) { const articles = await getAllArticles(); const searchParams = parseResourceUrl(req.url); @@ -44,16 +47,12 @@ export default function Greet( - {articles?.map((doc) => { - return ( - - ); - })} + {articles?.map((doc) => ( + + ))} ); diff --git a/routes/index.tsx b/routes/index.tsx index e1263f8..a6d523f 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -15,8 +15,9 @@ export default function Home(props: PageProps) { -
- {Object.values(resources).map((m) => { +

Resources

+
+ {Object.values(resources).filter((v) => v.link !== "/").map((m) => { return ( = { +export const handler: Handlers< + { movies: Movie[] | null; searchResults?: SearchResult } +> = { async GET(req, ctx) { const movies = await getAllMovies(); const searchParams = parseResourceUrl(req.url); @@ -46,7 +48,7 @@ export default function Greet( {movies?.map((doc) => { - return ; + return ; })} diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index 5a5fd46..756598b 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -1,5 +1,4 @@ import { Handlers, PageProps } from "$fresh/server.ts"; -import { RecipeCard } from "@components/RecipeCard.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; import { Grid } from "@components/Grid.tsx"; @@ -8,8 +7,11 @@ import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { SearchResult } from "@lib/types.ts"; +import { ResourceCard } from "@components/Card.tsx"; -export const handler: Handlers = { +export const handler: Handlers< + { recipes: Recipe[] | null; searchResults?: SearchResult } +> = { async GET(req, ctx) { const recipes = await getAllRecipes(); const searchParams = parseResourceUrl(req.url); @@ -45,7 +47,7 @@ export default function Greet( {recipes?.map((doc) => { - return ; + return ; })} diff --git a/routes/series/[name].tsx b/routes/series/[name].tsx index 10ac134..72f10cd 100644 --- a/routes/series/[name].tsx +++ b/routes/series/[name].tsx @@ -7,7 +7,7 @@ import { getSeries, Series } from "@lib/resource/series.ts"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { KMenu } from "@islands/KMenu.tsx"; -export const handler: Handlers = { +export const handler: Handlers<{ serie: Series; session: unknown }> = { async GET(_, ctx) { const serie = await getSeries(ctx.params.name); return ctx.render({ serie, session: ctx.state.session }); diff --git a/routes/series/index.tsx b/routes/series/index.tsx index ef812b9..7fda3c9 100644 --- a/routes/series/index.tsx +++ b/routes/series/index.tsx @@ -5,11 +5,13 @@ import { IconArrowLeft } from "@components/icons.tsx"; import { getAllSeries, Series } from "@lib/resource/series.ts"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { KMenu } from "@islands/KMenu.tsx"; -import { MovieCard } from "@components/MovieCard.tsx"; +import { ResourceCard } from "@components/Card.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { SearchResult } from "@lib/types.ts"; -export const handler: Handlers = { +export const handler: Handlers< + { series: Series[] | null; searchResults?: SearchResult } +> = { async GET(req, ctx) { const series = await getAllSeries(); const searchParams = parseResourceUrl(req.url); @@ -46,7 +48,7 @@ export default function Greet( {series?.map((doc) => { - return ; + return ; })} diff --git a/static/global.css b/static/global.css index 0d50f52..2f5a296 100644 --- a/static/global.css +++ b/static/global.css @@ -1,13 +1,4 @@ -.custom-grid { - grid-template-columns: repeat(auto-fit, minmax(37vw, 1fr)) ; -} - -@media(min-width: 640px){ - .custom-grid { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) ; - } -} .animate-hover { animation: hover 4s infinite; @@ -31,6 +22,18 @@ input::-webkit-inner-spin-button { /* Firefox */ input[type=number] { -moz-appearance: textfield; + appearance: textfield; +} + +.fix-border::before { + content: ""; + width: calc(100% + 2px); + height: calc(100% + 2px); + position: absolute; + top: 0px; + left: 0px; + border: solid thin #141218; + border-radius: 1.5rem; } .noisy-gradient::after { diff --git a/static/thumbnails.js b/static/thumbnails.js index fedb30c..75d00db 100644 --- a/static/thumbnails.js +++ b/static/thumbnails.js @@ -248,12 +248,16 @@ document.querySelectorAll("[data-thumb]").forEach((entry) => { const child = entry.querySelector("img[data-thumb-img]"); setTimeout(() => { const isLoaded = child && child.complete && child.naturalHeight !== 0; - console.log(isLoaded, child.getAttribute("src")); if (child && !isLoaded) { child.style.opacity = 0; + child.style.filter = "blur(5px)"; child.addEventListener("load", () => { - child.style.transition = "opacity 0.3s ease"; + child.style.transition = "opacity 0.3s ease, filter 0.6s ease"; child.style.opacity = 1; + child.style.filter = "blur(0px)"; + setTimeout(() => { + entry.style.background = ""; + }, 400); }); } }, 50);