feat: support platform icon and adjust breakpoint of gird
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
b75c58b83c
commit
dd74a99bcb
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path fill="#10161F" fill-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2M6.987 14.789a.75.75 0 0 0-.474 1.423c.183.058.345.328.487.566.07.115.134.223.195.296.227.273.537.567.985.757.31.132.658.2 1.046.202l-.005.457a1 1 0 0 0 2 .02l.029-2.745v-.01c0-.215.09-.408.236-.546.604-.57.296-1.561-.493-1.719-1.74-.347-2.66-1.446-2.66-2.402 0-.447.181-.896.556-1.305.264-.287.337-.702.189-1.063a1.402 1.402 0 0 1-.091-.457c.209.098.424.223.621.376.256.198.605.261.913.166.45-.138.95-.217 1.479-.217.529 0 1.028.079 1.477.217.309.095.657.031.913-.166.197-.153.412-.278.62-.376-.008.171-.037.329-.09.456-.148.362-.074.776.19 1.063.375.41.556.859.556 1.306 0 .956-.92 2.055-2.659 2.402-.789.158-1.097 1.15-.493 1.719a.745.745 0 0 1 .236.542l-.029 2.739a1 1 0 1 0 2 .02l.029-2.745v-.01c0-.239-.03-.47-.088-.691 1.69-.714 3.004-2.131 3.004-3.976 0-.82-.272-1.572-.72-2.207.091-.471.076-.924.034-1.27-.046-.377-.121-.797-.329-1.123a1.054 1.054 0 0 0-.414-.377c-.458-.23-1.027-.054-1.482.101a5.77 5.77 0 0 0-1.17.553A7.119 7.119 0 0 0 12 6.588c-.549 0-1.082.062-1.588.177a5.771 5.771 0 0 0-1.17-.553c-.454-.155-1.024-.33-1.481-.101-.17.085-.313.217-.414.377-.208.326-.283.747-.33 1.124-.041.347-.057.8.034 1.271a3.813 3.813 0 0 0-.718 2.205c0 1.845 1.315 3.262 3.005 3.976-.057.219-.088.448-.088.685l-.008.784c-.22 0-.37-.038-.476-.083a1.106 1.106 0 0 1-.419-.336c-.096-.116-.182-.238-.268-.361-.064-.093-.129-.185-.198-.274-.179-.232-.457-.545-.894-.69" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path fill="#10161F" fill-rule="evenodd" d="M12 4.25c-2.214 0-4.571.151-6.248.287a3.552 3.552 0 0 0-3.266 3.198C2.366 8.97 2.25 10.537 2.25 12c0 1.463.116 3.03.236 4.265a3.552 3.552 0 0 0 3.266 3.198c1.677.136 4.034.287 6.248.287 2.214 0 4.571-.151 6.248-.287a3.552 3.552 0 0 0 3.267-3.198c.12-1.234.235-2.802.235-4.265 0-1.463-.116-3.03-.235-4.265a3.552 3.552 0 0 0-3.267-3.198C16.571 4.401 14.214 4.25 12 4.25m-1.802 4.875c.482-.278.957-.023 1.908.486a31.144 31.144 0 0 1 1.694.978c.911.566 1.367.85 1.367 1.406 0 .557-.456.839-1.368 1.404a31.942 31.942 0 0 1-1.697.98c-.947.508-1.421.763-1.904.485-.482-.278-.499-.816-.533-1.892a31.058 31.058 0 0 1 0-1.951c.034-1.078.051-1.618.533-1.896" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 850 B |
|
|
@ -1,6 +1,8 @@
|
|||
import { cn } from "@renderer/lib/utils"
|
||||
import { parse } from "tldts"
|
||||
|
||||
import { PlatformIcon } from "./ui/platform-icon"
|
||||
|
||||
export function SiteIcon({
|
||||
url,
|
||||
className,
|
||||
|
|
@ -19,27 +21,36 @@ export function SiteIcon({
|
|||
try {
|
||||
host = new URL(url).host
|
||||
const pureDomain = parse(host).domainWithoutSuffix
|
||||
fallback = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain?.slice(0, 2).toUpperCase()}`
|
||||
fallback = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain
|
||||
?.slice(0, 2)
|
||||
.toUpperCase()}`
|
||||
src = `https://unavatar.follow.is/${host}?fallback=${fallback}`
|
||||
} catch {
|
||||
const pureDomain = parse(url).domainWithoutSuffix
|
||||
src = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain?.slice(0, 2).toUpperCase()}`
|
||||
src = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain
|
||||
?.slice(0, 2)
|
||||
.toUpperCase()}`
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
className={cn("mr-2 size-5 shrink-0 rounded-sm", className)}
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
if (fallback && e.currentTarget.src !== fallback) {
|
||||
e.currentTarget.src = fallback
|
||||
} else {
|
||||
// Empty svg
|
||||
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3C/svg%3E"
|
||||
}
|
||||
}}
|
||||
<PlatformIcon
|
||||
url={url}
|
||||
style={style}
|
||||
/>
|
||||
className={cn("mr-2 size-5 shrink-0 rounded-sm", className)}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
if (fallback && e.currentTarget.src !== fallback) {
|
||||
e.currentTarget.src = fallback
|
||||
} else {
|
||||
// Empty svg
|
||||
e.currentTarget.src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3C/svg%3E"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PlatformIcon>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { getSupportedPlatformIconName } from "./utils"
|
||||
|
||||
export const PlatformIcon: FC<{
|
||||
url: string
|
||||
children: React.JSX.Element
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}> = ({ className, url, children, style, ...rest }) => {
|
||||
const iconName = getSupportedPlatformIconName(url)
|
||||
|
||||
if (!iconName) {
|
||||
return (
|
||||
<Slot className={className} style={style} {...rest}>
|
||||
{children}
|
||||
</Slot>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<i
|
||||
className={cn(`${IconMap[iconName]} ${className}`)}
|
||||
style={{
|
||||
...style,
|
||||
maskImage: "var(--svg)",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const IconMap = {
|
||||
github: tw`i-mgc-github-2-cute-fi text-black dark:text-white`,
|
||||
twitter: tw`i-mgc-twitter-cute-fi text-[#55acee]`,
|
||||
x: tw`i-mgc-social-x-cute-li`,
|
||||
youtube: tw`i-mgc-youtube-cute-fi text-[#ff0000]`,
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
isGithubUrl,
|
||||
isTwitterUrl,
|
||||
isXUrl,
|
||||
isYoutubeUrl,
|
||||
} from "@renderer/lib/link-parser"
|
||||
|
||||
export const getSupportedPlatformIconName = (url: string) => {
|
||||
const safeUrl = parseSafeUrl(url)
|
||||
|
||||
if (!safeUrl) {
|
||||
return false
|
||||
}
|
||||
switch (true) {
|
||||
case isGithubUrl(safeUrl): {
|
||||
return "github"
|
||||
}
|
||||
case isTwitterUrl(safeUrl): {
|
||||
return "twitter"
|
||||
}
|
||||
case isXUrl(safeUrl): {
|
||||
return "x"
|
||||
}
|
||||
case isYoutubeUrl(safeUrl): {
|
||||
return "youtube"
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const parseSafeUrl = (url: string) => {
|
||||
try {
|
||||
return new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
export const getTweetId = (url: URL) => url.pathname.split("/").pop()!
|
||||
|
||||
const GITHUB_HOST = "github.com"
|
||||
|
||||
export const isGithubRepoUrl = (url: URL) =>
|
||||
url.hostname === GITHUB_HOST &&
|
||||
url.pathname.startsWith("/") &&
|
||||
url.pathname.split("/").length === 3
|
||||
|
||||
export const isGithubPrUrl = (url: URL) =>
|
||||
url.hostname === GITHUB_HOST && url.pathname.includes("/pull/")
|
||||
|
||||
export const isYoutubeUrl = (url: URL) =>
|
||||
url.hostname === "www.youtube.com" && url.pathname.startsWith("/watch")
|
||||
|
||||
export const isGistUrl = (url: URL) => url.hostname === "gist.github.com"
|
||||
|
||||
export const isGithubCommitUrl = (url: URL) => {
|
||||
const [_, , , type] = url.pathname.split("/")
|
||||
return url.hostname === GITHUB_HOST && type === "commit"
|
||||
}
|
||||
|
||||
export const isGithubProfileUrl = (url: URL) =>
|
||||
url.hostname === GITHUB_HOST && url.pathname.split("/").length === 2
|
||||
|
||||
export const isGithubFilePreviewUrl = (url: URL) => {
|
||||
// https://github.com/Innei/sprightly/blob/14234594f44956e6f56f1f92952ce82db37ef4df/src/socket/handler.ts
|
||||
const [_, , , type] = url.pathname.split("/")
|
||||
return url.hostname === GITHUB_HOST && type === "blob"
|
||||
}
|
||||
|
||||
export const isTweetUrl = (url: URL) =>
|
||||
isTwitterUrl(url) && url.pathname.startsWith("/")
|
||||
|
||||
export const isTwitterProfileUrl = (url: URL) =>
|
||||
isTwitterUrl(url) && url.pathname.split("/").length === 2
|
||||
|
||||
export const isGithubUrl = (url: URL) => url.hostname === GITHUB_HOST
|
||||
|
||||
export const isTwitterUrl = (url: URL) => url.hostname === "twitter.com"
|
||||
export const isXUrl = (url: URL) => url.hostname === "x.com"
|
||||
export const isXOrTwitterUrl = (url: URL) => isXUrl(url) || isTwitterUrl(url)
|
||||
|
||||
export const isTelegramUrl = (url: URL) => url.hostname === "t.me"
|
||||
|
||||
export const isCodesandboxUrl = (url: URL) =>
|
||||
// https://codesandbox.io/s/framer-motion-layoutroot-prop-forked-p39g96
|
||||
url.hostname === "codesandbox.io" && url.pathname.split("/").length === 3
|
||||
|
||||
export const isBilibiliUrl = (url: URL) =>
|
||||
url.hostname.includes("bilibili.com")
|
||||
|
||||
export const isBilibiliVideoUrl = (url: URL) =>
|
||||
isBilibiliUrl(url) && url.pathname.startsWith("/video/BV")
|
||||
|
||||
export const isZhihuUrl = (url: URL) => url.hostname === "www.zhihu.com"
|
||||
|
||||
export const isZhihuProfileUrl = (url: URL) =>
|
||||
isZhihuUrl(url) && url.pathname.startsWith("/people/")
|
||||
|
||||
export const isWikipediaUrl = (url: URL) =>
|
||||
url.hostname.includes("wikipedia.org")
|
||||
|
||||
export const isTMDBUrl = (url: URL) => url.hostname.includes("themoviedb.org")
|
||||
|
||||
export const isNpmUrl = (url: URL) => url.hostname.includes("npmjs.com")
|
||||
|
||||
export const isMozillaUrl = (url: URL) => url.hostname.includes("mozilla.org")
|
||||
|
||||
export const parseSelfArticleUrl = (url: URL) => {
|
||||
const [_, type, ...rest] = url.pathname.split("/")
|
||||
switch (type) {
|
||||
case "posts": {
|
||||
return {
|
||||
type,
|
||||
slug: rest.join("/"),
|
||||
}
|
||||
}
|
||||
case "notes": {
|
||||
return {
|
||||
type,
|
||||
nid: +rest[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGithubRepoUrl = (url: URL) => {
|
||||
const [_, owner, repo] = url.pathname.split("/")
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGithubGistUrl = (url: URL) => {
|
||||
const [_, owner, id] = url.pathname.split("/")
|
||||
return {
|
||||
owner,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGithubTypedUrl = (url: URL) => {
|
||||
// https://github.com/Innei/sprightly/blob/14234594f44956e6f56f1f92952ce82db37ef4df/src/socket/handler.ts
|
||||
// https://github.com/mx-space/core/commit/e1b4d881cf18e1cb66294d2620eada35937d9a12
|
||||
const split = url.pathname.split("/")
|
||||
const [_, owner, repo, type, id] = split
|
||||
|
||||
const afterTypeString = split.slice(4).join("/")
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
type,
|
||||
id,
|
||||
afterTypeString,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseZhihuProfileUrl = (url: URL) => {
|
||||
const [_, type, id] = url.pathname.split("/")
|
||||
return {
|
||||
type,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGithubPrUrl = (url: URL) => {
|
||||
const [_, owner, repo, type, pr] = url.pathname.split("/")
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
type,
|
||||
pr,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTMDBUrl = (url: URL) => {
|
||||
const [_, type, id] = url.pathname.split("/")
|
||||
return {
|
||||
type,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseBilibiliVideoUrl = (url: URL) => {
|
||||
// https://www.bilibili.com/video/BV1tj42197hU
|
||||
const [_, type, id] = url.pathname.split("/")
|
||||
return {
|
||||
type,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ export function GridItem({
|
|||
{children}
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-1 px-2 pb-3 pt-1 text-sm",
|
||||
"relative flex-1 px-2 py-1 text-sm",
|
||||
!asRead &&
|
||||
"before:absolute before:-left-1 before:top-[13.5px] before:block before:size-2 before:rounded-full before:bg-theme-accent",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export function EntryColumn() {
|
|||
ref={scrollRef}
|
||||
flex
|
||||
rootClassName="h-full"
|
||||
viewportClassName="[&>div]:grow flex pt-3"
|
||||
viewportClassName="[&>div]:grow flex pt-3 @container"
|
||||
>
|
||||
{virtuosoOptions.totalCount === 0 ? (
|
||||
entries.isLoading ?
|
||||
|
|
@ -182,7 +182,7 @@ export function EntryColumn() {
|
|||
) : view && views[view].gridMode ?
|
||||
(
|
||||
<VirtuosoGrid
|
||||
listClassName="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 px-4"
|
||||
listClassName={tw`grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4 @7xl:grid-cols-5 px-4 gap-1.5`}
|
||||
{...virtuosoOptions}
|
||||
ref={virtuosoRef}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export const UserProfileModalContent: FC<{
|
|||
</m.div>
|
||||
</div>
|
||||
<ScrollArea.ScrollArea
|
||||
mask
|
||||
ref={setScrollerRef}
|
||||
rootClassName="mb-4 h-[400px] grow w-[70ch] max-w-full px-5"
|
||||
viewportClassName="[&>div]:space-y-4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue