diff --git a/.eslintrc.json b/.eslintrc.json index fb9f920d..f5b3a4f9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,6 +27,12 @@ "../*", "@/app/blog/*", "**/*.stories" + ], + "paths": [ + { + "name": "tailwind-merge", + "message": "Please import from @/lib/tailwind/merge instead." + } ] } ], diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ae29c762..4c2ce15b 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -11,12 +11,10 @@ on: jobs: build: - runs-on: ubuntu-latest - strategy: matrix: - node-version: [18.x] + node-version: [20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/bun.lockb b/bun.lockb index 4350b252..a8c747da 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/mdx-components.tsx b/mdx-components.tsx index 5e157b1d..2e2fa7da 100644 --- a/mdx-components.tsx +++ b/mdx-components.tsx @@ -1,3 +1,8 @@ +import * as React from 'react' + +import { InlineLink } from '@/components/atoms/InlineLink' +import * as Wrapper from '@/components/wrappers' + import type { MDXComponents } from 'mdx/types' // This file is required to use MDX in `app` directory. @@ -6,5 +11,61 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { // Allows customizing built-in components, e.g. to add styling. // h1: ({ children }) =>

{children}

, ...components, + + h2: props => { + // props contains legacyRef, so we need to remove it. + const { ref, ...rest } = props + return + }, + + h3: props => { + const { ref, ...rest } = props + return + }, + + h4: props => { + const { ref, ...rest } = props + return + }, + + h5: props => { + const { ref, ...rest } = props + return + }, + + hr: props => { + const { ref, ...rest } = props + return + }, + + a: props => { + const { ref, ...rest } = props + return + }, + + kbd: props => { + const { ref, ...rest } = props + return + }, + + ul: props => { + const { ref, ...rest } = props + return + }, + + ol: props => { + const { ref, ...rest } = props + return + }, + + li: props => { + const { ref, ...rest } = props + return + }, + + table: props => { + const { ref, ...rest } = props + return + }, } } diff --git a/package.json b/package.json index 35b648a2..73bf0e59 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trpfrog-net", "private": true, "scripts": { - "dev": "prisma generate && npx concurrently 'next dev' 'bun tools/markdownWatcher.ts'", + "dev": "prisma generate && npx concurrently 'next dev --turbo' 'bun tools/markdownWatcher.ts'", "build": "prisma generate && next build", "analyze": "ANALYZE=true next build", "start": "next start", @@ -21,58 +21,59 @@ "src/**/*.{js,jsx,ts,tsx}": "eslint --cache --fix" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.4.2", - "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@langchain/openai": "^0.0.14", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", - "@next/bundle-analyzer": "^13.4.19", - "@next/mdx": "^13.4.19", - "@octokit/rest": "^19.0.13", - "@prisma/client": "^4.16.2", + "@next/bundle-analyzer": "^14.1.0", + "@next/mdx": "^14.1.0", + "@next/third-parties": "canary", + "@prisma/client": "^5.9.1", "@react-hookz/web": "^23.1.0", - "@vercel/edge-config": "^0.2.1", - "@vercel/kv": "^0.2.2", - "@vercel/toolbar": "^0.1.4", - "better-react-mathjax": "^2.0.2", - "budoux": "^0.6.0", - "bufferutil": "^4.0.7", - "chokidar": "^3.5.3", - "classnames": "^2.3.2", - "cloudinary": "^1.40.0", - "dayjs": "^1.11.9", + "@tailwindcss/container-queries": "^0.1.1", + "@vercel/edge-config": "^1.0.2", + "@vercel/kv": "^1.0.1", + "@vercel/toolbar": "^0.1.11", + "better-react-mathjax": "^2.0.3", + "budoux": "^0.6.2", + "bufferutil": "^4.0.8", + "chokidar": "^3.6.0", + "classnames": "^2.5.1", + "cloudinary": "^2.0.1", + "dayjs": "^1.11.10", + "devicon": "^2.16.0", "easymde": "^2.18.0", - "eslint-config-next": "^14.0.3", - "framer-motion": "^10.16.2", + "eslint-config-next": "^14.1.0", + "framer-motion": "^11.0.5", "gray-matter": "^4.0.3", - "http-status-codes": "^2.2.0", - "jotai": "^2.4.1", + "http-status-codes": "^2.3.0", + "jotai": "^2.6.4", "js-yaml": "^4.1.0", - "langchain": "^0.0.140", - "lru-cache": "^9.1.2", - "microcms-js-sdk": "^2.5.0", - "next": "^14.0.3", - "next-cloudinary": "^4.20.0", + "langchain": "^0.1.20", + "lru-cache": "^10.2.0", + "microcms-js-sdk": "^2.7.0", + "next": "^14.1.0", "next-mdx-remote": "^4.4.1", "nookies": "^2.5.2", - "open-graph-scraper": "^6.3.2", - "prisma": "^4.16.2", + "open-graph-scraper": "^6.4.0", + "prisma": "^5.9.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", + "react-hook-form": "^7.50.1", "react-hot-toast": "^2.4.1", - "react-lite-youtube-embed": "^2.3.52", - "react-loading-skeleton": "^3.3.1", - "react-markdown": "^8.0.7", + "react-lite-youtube-embed": "^2.4.0", + "react-loading-skeleton": "^3.4.0", "react-modal": "^3.16.1", - "react-player": "^2.12.0", + "react-player": "^2.14.1", "react-rewards": "^2.0.4", "react-simplemde-editor": "^5.2.0", "react-string-replace": "^1.1.1", "react-syntax-highlighter": "^15.5.0", - "react-tooltip": "^5.21.5", - "react-tweet": "^3.1.1", + "react-tooltip": "^5.26.3", + "react-tweet": "^3.2.0", "react-twitter-widgets": "^1.11.0", "react-wrap-balancer": "^1.1.0", "react-youtube": "^10.1.0", @@ -84,54 +85,55 @@ "remark-toc": "^8.0.1", "remark-unwrap-images": "^3.0.1", "server-only": "^0.0.1", - "sharp": "^0.32.6", - "socket.io": "^4.7.2", - "socket.io-client": "^4.7.2", - "swr": "^2.2.2", - "textlint": "^13.3.3", - "textlint-rule-preset-japanese": "^7.0.0", + "socket.io": "^4.7.4", + "socket.io-client": "^4.7.4", + "swr": "^2.2.5", + "tailwind-variants": "^0.2.0", "use-sound": "^4.0.1", - "utf-8-validate": "^5.0.10", - "zod": "^3.22.2" + "zod": "^3.22.4" }, "devDependencies": { - "@storybook/addon-essentials": "^7.4.2", - "@storybook/addon-interactions": "^7.4.2", - "@storybook/addon-links": "^7.4.2", - "@storybook/addon-onboarding": "^1.0.8", - "@storybook/blocks": "^7.4.2", - "@storybook/manager-api": "^7.4.2", - "@storybook/nextjs": "^7.4.2", - "@storybook/react": "^7.4.2", + "@storybook/addon-essentials": "^7.6.16", + "@storybook/addon-interactions": "^7.6.16", + "@storybook/addon-links": "^7.6.16", + "@storybook/addon-onboarding": "^1.0.11", + "@storybook/blocks": "^7.6.16", + "@storybook/manager-api": "^7.6.16", + "@storybook/nextjs": "^7.6.16", + "@storybook/react": "^7.6.16", "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^7.4.2", - "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.0.0", - "@types/jest": "^29.5.5", - "@types/js-yaml": "^4.0.5", - "@types/node": "^18.17.13", - "@types/react": "^18.2.21", - "@types/react-modal": "^3.16.0", - "@types/react-syntax-highlighter": "^15.5.7", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", + "@storybook/theming": "^7.6.16", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@types/jest": "^29.5.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.11.19", + "@types/react": "^18.2.57", + "@types/react-modal": "^3.16.3", + "@types/react-syntax-highlighter": "^15.5.11", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "autoprefixer": "^10.4.17", "clipboardy": "^4.0.0", - "eslint": "^8.53.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-storybook": "^0.6.15", - "eslint-plugin-testing-library": "^6.1.0", - "eslint-plugin-unused-imports": "^3.0.0", - "husky": "^8.0.3", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-testing-library": "^6.2.0", + "eslint-plugin-unused-imports": "^3.1.0", + "husky": "^9.0.11", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "lint-staged": "^14.0.1", - "prettier": "3.0.3", - "sass": "^1.66.1", - "storybook": "^7.4.2", - "storybook-dark-mode": "^3.0.1", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "typescript": "^5.2.2" + "lint-staged": "^15.2.2", + "postcss": "^8.4.35", + "prettier": "3.2.5", + "prettier-plugin-tailwindcss": "^0.5.11", + "sass": "^1.71.0", + "storybook": "^7.6.16", + "storybook-dark-mode": "^3.0.3", + "tailwindcss": "^3.4.1", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/prettier.config.js b/prettier.config.js index a0cdb875..d9473af7 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -2,12 +2,14 @@ /** @type {import("prettier").Config} */ const config = { + plugins: ['prettier-plugin-tailwindcss'], trailingComma: 'all', tabWidth: 2, semi: false, singleQuote: true, jsxSingleQuote: false, arrowParens: 'avoid', + tailwindFunctions: ['tv'], } module.exports = config diff --git a/src/app/(gallery)/_components/ImageList/index.tsx b/src/app/(gallery)/_components/ImageList.tsx similarity index 83% rename from src/app/(gallery)/_components/ImageList/index.tsx rename to src/app/(gallery)/_components/ImageList.tsx index 5f6571f1..62fd5e6f 100644 --- a/src/app/(gallery)/_components/ImageList/index.tsx +++ b/src/app/(gallery)/_components/ImageList.tsx @@ -1,8 +1,6 @@ import Image from 'next/legacy/image' import Link from 'next/link' -import styles from './index.module.scss' - export type ImageListProps = { images: ImagePaths[] imageWidth?: number @@ -18,7 +16,7 @@ export type ImagePaths = { export function ImageList(props: ImageListProps) { return ( -
+
{props.images.map(({ src, url, alt }, idx) => ( {alt diff --git a/src/app/(gallery)/_components/ImageList/index.module.scss b/src/app/(gallery)/_components/ImageList/index.module.scss deleted file mode 100644 index 9b48eb6b..00000000 --- a/src/app/(gallery)/_components/ImageList/index.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.icon_grid { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); - margin: 30px 0; -} diff --git a/src/app/(gallery)/_components/ImageNavigation/index.tsx b/src/app/(gallery)/_components/ImageNavigation.tsx similarity index 66% rename from src/app/(gallery)/_components/ImageNavigation/index.tsx rename to src/app/(gallery)/_components/ImageNavigation.tsx index 3601196c..ce64d113 100644 --- a/src/app/(gallery)/_components/ImageNavigation/index.tsx +++ b/src/app/(gallery)/_components/ImageNavigation.tsx @@ -1,9 +1,8 @@ import * as React from 'react' import Image from 'next/legacy/image' -import Link from 'next/link' -import styles from './index.module.scss' +import { MagicButton } from '@/components/atoms/MagicButton' type ImageNavigationProps = { icons: { @@ -17,10 +16,8 @@ type ImageNavigationProps = { export function ImageNavigation(props: ImageNavigationProps) { return ( -
- - ← - +
+ {props.icons.map(nav => { return ( ) })} - - → - +
) } diff --git a/src/app/(gallery)/_components/ImageNavigation/index.module.scss b/src/app/(gallery)/_components/ImageNavigation/index.module.scss deleted file mode 100644 index 5c1e3237..00000000 --- a/src/app/(gallery)/_components/ImageNavigation/index.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -.thumbnails { - display: grid; - grid-template-rows: 1fr; - grid-template-columns: repeat(7, 1fr); -} - -.prev_image, -.next_image { - display: flex; - align-items: center; - justify-content: center; - background: var(--link-button-color); - border-radius: 10px; - color: white !important; - text-decoration: none; - font-weight: bold; - margin: 10px; - height: 60%; -} diff --git a/src/app/(gallery)/_components/ImageViewer/index.tsx b/src/app/(gallery)/_components/ImageViewer.tsx similarity index 72% rename from src/app/(gallery)/_components/ImageViewer/index.tsx rename to src/app/(gallery)/_components/ImageViewer.tsx index 6cd958a5..5ed62d92 100644 --- a/src/app/(gallery)/_components/ImageViewer/index.tsx +++ b/src/app/(gallery)/_components/ImageViewer.tsx @@ -1,7 +1,5 @@ import Image from 'next/legacy/image' -import styles from './index.module.scss' - type ImageViewerProps = { src: string alt: string @@ -9,8 +7,8 @@ type ImageViewerProps = { export function ImageViewer(props: ImageViewerProps) { return ( -
-
+
+
+ @@ -50,7 +51,7 @@ export default function Index(context: PageProps) {
- + 一覧に戻る
diff --git a/src/app/(gallery)/icons/page.tsx b/src/app/(gallery)/icons/page.tsx index 8f60fed9..b18cc878 100644 --- a/src/app/(gallery)/icons/page.tsx +++ b/src/app/(gallery)/icons/page.tsx @@ -2,11 +2,12 @@ import { Metadata } from 'next' import { ImagePaths, ImageList } from '@/app/(gallery)/_components/ImageList' -import { Button } from '@/components/atoms/Button' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' +import { MagicButton } from 'src/components/atoms/MagicButton' + export const metadata = { title: 'つまみアイコン集', description: 'つまみちゃんの作ったアイコンです。', @@ -21,19 +22,19 @@ export default function Index() { }) return ( - + <p> つまみちゃんの作ったアイコンです。クリックで高解像度版に飛びます。 </p> <p>Hugging Face Datasets でも利用可能です!</p> <p> - <Button + <MagicButton externalLink={true} href={'https://huggingface.co/datasets/TrpFrog/trpfrog-icons'} > trpfrog-icons on 🤗Datasets - </Button> + </MagicButton> </p> @@ -45,12 +46,12 @@ export default function Index() { にて使えるようになりました!🎉

- +

+    
       
         
       
@@ -51,7 +52,7 @@ export default function Index(context: PageProps) {
       
       
         
- + 一覧に戻る
diff --git a/src/app/(gallery)/stickers/page.tsx b/src/app/(gallery)/stickers/page.tsx index b52a4106..d7a64c9d 100644 --- a/src/app/(gallery)/stickers/page.tsx +++ b/src/app/(gallery)/stickers/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import { ImagePaths, ImageList } from '@/app/(gallery)/_components/ImageList' +import { InlineLink } from '@/components/atoms/InlineLink' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' @@ -20,16 +21,18 @@ export default function Index() { }) return ( - + <p> つまみスタンプの元画像の5倍に拡大したやつです。 <br /> 良識の範囲内でご自由にどうぞ。(Twitterの会話とか) </p> - <a href={'https://store.line.me/stickershop/product/8879469/ja'}> + <InlineLink + href={'https://store.line.me/stickershop/product/8879469/ja'} + > LINEスタンプ発売中! - </a> + </InlineLink> diff --git a/src/app/(gallery)/stickers/style.module.scss b/src/app/(gallery)/stickers/style.module.scss deleted file mode 100644 index dc947b3a..00000000 --- a/src/app/(gallery)/stickers/style.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.icon_grid { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); - margin: 30px 0; -} - -.icon_block { - display: inline-block; - background: var(--window-bkg-color); - padding: 10px 20px; - border-radius: 20px; -} diff --git a/src/app/(home)/_cards/AICard/AICard.tsx b/src/app/(home)/_cards/AICard/AICard.tsx new file mode 100644 index 00000000..a443142c --- /dev/null +++ b/src/app/(home)/_cards/AICard/AICard.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' + +import { TopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +import { useTooltip } from '@/hooks/useTooltip' + +import { tv } from '@/lib/tailwind/variants' + +import { IconFrame } from './IconFrame' + +const styles = tv({ + slots: { + card: 'tw-grid', + button: cardButtonStyle({ + class: 'tw-absolute tw-bottom-2 tw-right-2 tw-size-8 tw-rounded-full', + invertColor: true, + }), + description: + '!tw-text-center !tw-text-xs dark:!tw-bg-text-color dark:!tw-text-window-color', + poweredBy: 'tw-text-center tw-text-xs tw-text-gray-500', + }, +})() + +export function AICard() { + const { TooltipContent, TooltipButton } = useTooltip() + + return ( + + + ? + + これは AI により自動生成された画像です。 +
+ 最後の生成から3時間経つと再生成されます。 +
+
+ ) +} diff --git a/src/app/(home)/_cards/AICard/IconFrame.tsx b/src/app/(home)/_cards/AICard/IconFrame.tsx new file mode 100644 index 00000000..5c65637d --- /dev/null +++ b/src/app/(home)/_cards/AICard/IconFrame.tsx @@ -0,0 +1,127 @@ +'use client' + +import * as React from 'react' + +import { InlineLink } from '@/components/atoms/InlineLink' +import { WaveText } from '@/components/atoms/WaveText' + +import { tv } from '@/lib/tailwind/variants' +import { ParseWithBudouX } from '@/lib/wordSplit' + +import { + TrpFrogDiffusionResult, + useTrpFrogDiffusion, +} from './useTrpFrogDiffusion' + +const createStyles = tv({ + slots: { + wrapper: 'tw-@container', + layout: 'tw-flex tw-h-full tw-flex-col @sm:tw-flex-row', + picture: [ + 'tw-aspect-square tw-w-full @sm:tw-w-1/3 @sm:tw-scale-105', + 'tw-font-mplus-rounded tw-text-2xl tw-font-bold', + 'tw-flex tw-items-center tw-justify-center', + ], + caption: [ + 'tw-flex-1 tw-gap-1 tw-px-2 tw-text-center', + 'tw-flex tw-flex-col tw-items-center tw-justify-center', + ], + english: + 'tw-text-balance tw-text-lg tw-font-black tw-italic tw-leading-tight', + japanese: 'tw-text-balance tw-text-[11px]', + aiGeneratedMsg: 'tw-text-[9px] tw-leading-none tw-text-gray-500', + poweredBy: 'tw-text-center tw-text-[10px] tw-leading-none tw-text-gray-500', + }, + variants: { + status: { + ok: { + english: ` + tw-bg-gradient-to-br + tw-from-blue-400 tw-to-pink-400 + tw-bg-clip-text tw-text-transparent + `, + }, + loading: { + picture: 'tw-bg-gray-200 tw-text-black', + english: 'tw-h-4 tw-w-2/3 tw-animate-pulse tw-rounded tw-bg-zinc-400', + japanese: 'tw-h-3 tw-w-1/3 tw-animate-pulse tw-rounded tw-bg-zinc-400', + }, + error: { + picture: 'tw-bg-red-800 tw-text-white', + }, + } satisfies Record, + }, + defaultVariants: { + status: 'ok', + }, +}) + +export function IconFrame() { + const { status, data } = useTrpFrogDiffusion() + + if (status === 'loading') { + const styles = createStyles({ status }) + return ( +
+
+
+ Loading... +
+
+
+
+
+
+
+ ) + } + + if (status === 'error') { + const styles = createStyles({ status }) + return ( +
+
+
+ Error occurred +
+
+

+ エラーが発生しました。 +
+ 時間をあけて再度お試しください。 +

+
+
+
+ ) + } + + const styles = createStyles() + const { base64, prompt, translated } = data + return ( +
+
+ {`Auto +
+
AI GENERATED ICON
+
{prompt}
+
+ +
+
+ Powered by{' '} + + Prgckwb/trpfrog-diffusion + +
+
+
+
+ ) +} diff --git a/src/app/(home)/_cards/AICard/index.ts b/src/app/(home)/_cards/AICard/index.ts new file mode 100644 index 00000000..458bf9d2 --- /dev/null +++ b/src/app/(home)/_cards/AICard/index.ts @@ -0,0 +1 @@ +export { AICard } from './AICard' diff --git a/src/app/(home)/_cards/AICard/useTrpFrogDiffusion.ts b/src/app/(home)/_cards/AICard/useTrpFrogDiffusion.ts new file mode 100644 index 00000000..d6930bf6 --- /dev/null +++ b/src/app/(home)/_cards/AICard/useTrpFrogDiffusion.ts @@ -0,0 +1,40 @@ +import { useCallback, useMemo } from 'react' + +import useSWR from 'swr' + +import type { TrpFrogImageGenerationResult } from '@/app/api/trpfrog-diffusion/route' + +export type TrpFrogDiffusionResult = + | { + data: undefined + status: 'loading' | 'error' + } + | { + data: TrpFrogImageGenerationResult + status: 'ok' + } + +export function useTrpFrogDiffusion() { + const fetcher = useCallback( + (url: string) => fetch(url).then(r => r.json()), + [], + ) + const { data, error, isLoading } = useSWR('/api/trpfrog-diffusion', fetcher) + + let status: TrpFrogDiffusionResult['status'] + if (isLoading) { + status = 'loading' + } else if (error || !data || !data.base64) { + status = 'error' + } else { + status = 'ok' + } + + return useMemo( + () => ({ + status, + data, + }), + [status, data], + ) +} diff --git a/src/app/(home)/_cards/AboutMeCard.tsx b/src/app/(home)/_cards/AboutMeCard.tsx new file mode 100644 index 00000000..a22c0d62 --- /dev/null +++ b/src/app/(home)/_cards/AboutMeCard.tsx @@ -0,0 +1,86 @@ +import { tv } from 'tailwind-variants' + +import { TopCard } from '@/app/(home)/_components/TopCard' + +const image = + 'https://res.cloudinary.com/trpfrog/blog/sugadaira-travel/42C94C5A-04C6-4DEC-9D41-2C87F87D79B7_1_105_c.jpg' + +export const attributes = [ + { icon: '🐸', iconName: '性別', text: '男性' }, + { icon: '🎂', iconName: '誕生日', text: '2000年10月17日 (23歳)' }, + { + icon: '🏠', + iconName: '出身', + text: '東京都 (23区外) 出身', + }, + { + icon: '🍎', + iconName: 'Apple', + text: 'Apple ユーザー', + }, + // { + // icon: '⌨️', + // iconName: 'キー配列', + // text: 'US 配列 (テンキーレス)', + // }, + // { + // icon: '🎓', + // iconName: '最終学歴', + // text: '電気通信大学', + // }, +] + +const styles = tv({ + slots: { + bgImage: ['tw-bg-cover tw-bg-center tw-bg-no-repeat sp:tw-bg-left'], + base: [ + 'tw-h-full tw-w-full tw-bg-gradient-to-br tw-from-window-color tw-to-transparent tw-p-8 sp:tw-p-5', + 'tw-flex tw-flex-col tw-justify-between', + ], + nameWrapper: 'tw-flex tw-items-baseline tw-gap-2 ', + name: 'first:tw-text-4xl first:tw-font-bold last:tw-text-2xl', + textWrapper: 'tw-text-justify tw-leading-7 sp:tw-text-sm sp:tw-leading-6', + text: 'tw-mr-1 tw-rounded-sm tw-bg-window-color/95 tw-leading-none dark:tw-bg-text-color/95', + introAttribute: + 'tw-relative -tw-left-1 tw-w-fit tw-list-none tw-rounded tw-bg-window-color/90 tw-px-2 tw-py-1.5 tw-text-xs', + attrItem: 'tw-flex tw-items-start tw-gap-2 tw-leading-relaxed', + }, +})() + +export function AboutMeCard() { + return ( + +
+
+

+ つまみ + (TrpFrog) +

+

+ + 自然言語生成の研究をしている大学院生 + ……のはずだが、Web開発に興味がありすぎてそういう職業になりそうになっている。 + 面白いものが好き、面白ければなんでもやりたい。 + みんなそう?そうかも + +

+
+
    + {attributes.map(({ icon, iconName, text }) => ( +
  • + + {icon} + + {text} +
  • + ))} +
+
+
+ ) +} diff --git a/src/app/(home)/_cards/BalloonCard/BalloonCard.tsx b/src/app/(home)/_cards/BalloonCard/BalloonCard.tsx new file mode 100644 index 00000000..bf7fca93 --- /dev/null +++ b/src/app/(home)/_cards/BalloonCard/BalloonCard.tsx @@ -0,0 +1,55 @@ +'use client' + +import React, { useRef } from 'react' + +import { TopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +import { A } from '@/components/wrappers' + +import { tv } from '@/lib/tailwind/variants' + +import { useResizableBalloonArray } from './useResizableBalloonArray' + +const createStyles = tv({ + slots: { + wrapper: + 'tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center sp:tw-h-32', + dialog: [ + 'tw-absolute tw-left-0 tw-top-0 tw-z-10 tw-h-full tw-w-full', + 'tw-flex tw-flex-col tw-items-center tw-justify-center', + 'tw-gap-1 tw-font-bold tw-backdrop-blur-sm', + ], + button: cardButtonStyle({ invertColor: true }), + }, + variants: { + isBurstAll: { + true: { + dialog: 'tw-opacity-100 tw-delay-500 tw-duration-700', + }, + false: { + dialog: 'tw-pointer-events-none tw-opacity-0 tw-duration-100', + }, + }, + }, +}) + +export function BalloonCard() { + const cardRef = useRef(null) + const { balloonComponent, isBurstAll } = useResizableBalloonArray(cardRef, 60) + const styles = createStyles({ isBurstAll }) + + return ( + +
+ {balloonComponent} +
+
+ Want more balloons popping? + + もっと割る + +
+
+ ) +} diff --git a/src/app/(home)/_cards/BalloonCard/useResizableBalloonArray.tsx b/src/app/(home)/_cards/BalloonCard/useResizableBalloonArray.tsx new file mode 100644 index 00000000..55af327e --- /dev/null +++ b/src/app/(home)/_cards/BalloonCard/useResizableBalloonArray.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useMemo, useState, useTransition } from 'react' + +import { BalloonArray } from '@/app/balloon/_components/BalloonArray' + +export function useResizableBalloonArray( + cardRef: React.RefObject, + balloonWidth: number, +) { + const [balloonAmount, setBalloonAmount] = useState(0) + const balloonHeight = balloonWidth / 0.6 + + useEffect(() => { + // リサイズ時、動的に風船の数を変更 + const observer = new ResizeObserver(() => { + if (!cardRef.current) return + + // TODO: EPS なしでもレイアウトが崩れないようにする + const EPS = 10 + const cardWidth = cardRef.current.clientWidth - EPS + const cardHeight = cardRef.current.clientHeight - EPS + + const horizontalBalloonAmount = Math.floor(cardWidth / balloonWidth) + const verticalBalloonAmount = Math.floor(cardHeight / balloonHeight) + const balloonAmount = horizontalBalloonAmount * verticalBalloonAmount + setBalloonAmount(balloonAmount) + }) + + if (cardRef.current) { + observer.observe(cardRef.current) + } + return () => observer.disconnect() + }, [balloonHeight, balloonWidth, cardRef]) + + // 全ての風船が割れたかどうか + const [isBurstAll, setIsBurstAll] = useState(false) + const [_, startTransition] = useTransition() + + return useMemo( + () => ({ + balloonComponent: ( + + // 割れてすぐに画面に反映させる必要はないので遅延させる + startTransition(() => setIsBurstAll(isBurst.every(v => v))) + } + key={balloonAmount} // 風船の数が変わったら state をリセット + /> + ), + isBurstAll, + }), + [balloonAmount, isBurstAll, balloonWidth], + ) +} diff --git a/src/app/(home)/_cards/BelongingCard.tsx b/src/app/(home)/_cards/BelongingCard.tsx new file mode 100644 index 00000000..784fdd89 --- /dev/null +++ b/src/app/(home)/_cards/BelongingCard.tsx @@ -0,0 +1,45 @@ +import { TopCard } from '@/app/(home)/_components/TopCard' + +import { tv } from '@/lib/tailwind/variants' + +const src = 'https://res.cloudinary.com/trpfrog/image/upload/w_500/IMG_6686.jpg' +const styles = tv({ + slots: { + wrapper: [ + 'tw-size-full tw-bg-gradient-to-r tw-from-yellow-800/20 tw-to-zinc-800/60', + ], + text: [ + 'tw-flex tw-size-full tw-flex-col tw-items-end tw-justify-center ', + 'tw-relative tw-top-1 tw-p-3 tw-text-right tw-text-white tw-drop-shadow-lg', + ], + name: 'tw-text-lg tw-font-bold', + details: 'tw-text-sm tw-font-bold', + theme: 'tw-text-xs', + date: 'tw-text-xs', + }, +})() + +export function BelongingCard() { + return ( + +
+
+

電気通信大学大学院

+
情報理工学研究科 情報学専攻
+
Since 2023.04
+
研究分野: 自然言語生成
+
+
+
+ ) +} diff --git a/src/app/(home)/_cards/BirdsCard.tsx b/src/app/(home)/_cards/BirdsCard.tsx new file mode 100644 index 00000000..0d69cd28 --- /dev/null +++ b/src/app/(home)/_cards/BirdsCard.tsx @@ -0,0 +1,61 @@ +import { TopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +import { useTooltip } from '@/hooks/useTooltip' + +import { tv } from '@/lib/tailwind/variants' + +const leftBird = ` + +   / ̄ ̄\ ムシャムシャ + /  (●)/ ̄ ̄\ +.  /    ト、   \ + 彳     \\  | +./   /⌒ヽヽ  | +/      |  | .|  /。 +    |  ヽ|/∴ +       。゜ +`.trimEnd() + +const rightBird = ` +オエーー!! ___ +    ___/     ヽ +   /   / /⌒ヽ| +/ (゚)/  / / +.  /    ト、 /。⌒ヽ。 + 彳    \\゚。∴。o +./     \\。゚。o +/     /⌒\U∴) +      |   ゙U | +      |    | | +        U +`.trimEnd() + +const styles = tv({ + slots: { + base: [ + 'tw-flex tw-items-center tw-justify-center tw-gap-6', + 'tw-font-mono tw-text-[11px] tw-leading-none', + 'tw-bg-text-color/80 tw-text-window-color', + ], + button: cardButtonStyle({ + class: 'tw-absolute tw-bottom-2 tw-right-2 tw-size-8 tw-rounded-full', + }), + tooltipContent: '!tw-bg-window-color !tw-text-text-color', + }, +})() + +export function BirdsCard() { + const { TooltipContent, TooltipButton } = useTooltip() + + return ( + +
{leftBird}
+
{rightBird}
+ ? + + なぜか2019年からトップページにいる、特に意味のない鳥です + +
+ ) +} diff --git a/src/app/(home)/_cards/BlogCard.tsx b/src/app/(home)/_cards/BlogCard.tsx new file mode 100644 index 00000000..abca6da5 --- /dev/null +++ b/src/app/(home)/_cards/BlogCard.tsx @@ -0,0 +1,128 @@ +import { tv, VariantProps } from 'tailwind-variants' + +import { TopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +import { A } from '@/components/wrappers' + +import { BlogPost } from '@blog/_lib/blogPost' +import { retrieveSortedBlogPostList } from '@blog/_lib/load' + +const createArticleStyle = tv({ + slots: { + bg: 'tw-h-full tw-w-full tw-bg-cover tw-bg-center tw-bg-no-repeat', + wrapper: [ + 'tw-flex tw-h-full tw-w-full tw-items-center tw-justify-between', + 'tw-h-full tw-p-4 tw-text-text-color tw-backdrop-blur-[2px]', + ], + info: 'tw-flex tw-w-2/3 tw-flex-col tw-text-white sp:tw-w-3/4', + button: cardButtonStyle(), + }, + variants: { + rightToLeft: { + true: { + wrapper: 'tw-flex-row-reverse tw-bg-gradient-to-l', + info: 'tw-items-end tw-text-right', + }, + false: { + wrapper: 'tw-bg-gradient-to-r', + }, + }, + index: { + 0: { + wrapper: ` + tw-from-pink-500 tw-to-pink-500/40 + dark:tw-from-pink-900 dark:tw-to-pink-900/40 + `, + button: 'tw-translate-y-3', + }, + 1: { + wrapper: ` + tw-from-amber-500 tw-to-amber-500/40 + dark:tw-from-amber-900 dark:tw-to-amber-900/40 + `, + }, + 2: { + wrapper: ` + tw-from-lime-600 tw-to-lime-600/40 + dark:tw-from-lime-900 dark:tw-to-lime-900/40 + `, + }, + }, + }, +}) + +function getCloudinaryResizedUrl(url: string, width = 600) { + if (/\/image\/upload\/.*\/blog/.test(url)) { + return url.replace( + /\/image\/upload\/.*\/blog/, + `/image/upload/w_${width}/blog`, + ) + } else if (url.includes('/image/upload/')) { + return url.replace('/image/upload/', `/image/upload/w_${width}/`) + } else { + return url.replace('trpfrog/blog', `trpfrog/w_${width}/blog`) + } +} + +function ArticleRow(props: { + entry: BlogPost + variant: Required> +}) { + const { entry, variant } = props + const articleStyle = createArticleStyle(variant) + const resizedThumbnailUrl = + entry.thumbnail && getCloudinaryResizedUrl(entry.thumbnail, 700) + return ( +
+
+
+

+ {entry.title} +

+
+ {entry.description} +
+
+ {entry.date}・{Math.floor(entry.readTime / 60)} min to read・ + {entry.tags.map(t => `#${t}`).join(' ')} +
+
+ + 記事を読む + + +
+
+ ) +} + +export async function BlogCard() { + const articles = await retrieveSortedBlogPostList() + + const latestFeaturedArticles = articles.filter(e => !!e.thumbnail).slice(0, 3) + + return ( + +
+ {latestFeaturedArticles.map((article, i) => { + const rightToLeft = i % 2 === 1 + const index = (i % 3) as 0 | 1 | 2 + return ( + + ) + })} +
+
+ ) +} diff --git a/src/app/(home)/_cards/CompetitiveCard.tsx b/src/app/(home)/_cards/CompetitiveCard.tsx new file mode 100644 index 00000000..2557e9ec --- /dev/null +++ b/src/app/(home)/_cards/CompetitiveCard.tsx @@ -0,0 +1,37 @@ +import { LinkTopCard } from '@/app/(home)/_components/TopCard' + +import { tv } from '@/lib/tailwind/variants' + +const styles = tv({ + slots: { + wrapper: [ + 'tw-flex tw-w-full tw-flex-col tw-items-center tw-justify-center', + 'tw-bg-zinc-800 tw-py-3', + ], + skill: 'tw-flex tw-flex-wrap tw-gap-2', + textGradient: [ + 'tw-flex tw-w-full tw-flex-col tw-items-center tw-justify-center', + 'tw-bg-gradient-to-br tw-from-cyan-100 tw-via-cyan-500 tw-to-cyan-100 tw-bg-clip-text', + 'tw-font-bold tw-text-transparent', + ], + rating: 'tw-text-7xl tw-font-bold', + }, +})() + +export function CompetitiveCard() { + return ( + +
+
+ max + 1596 +
+
AtCoder 水色レーティング
+
+
+ ) +} diff --git a/src/app/(home)/_cards/EnvironmentCard.tsx b/src/app/(home)/_cards/EnvironmentCard.tsx new file mode 100644 index 00000000..a4043e8d --- /dev/null +++ b/src/app/(home)/_cards/EnvironmentCard.tsx @@ -0,0 +1,34 @@ +import { LinkTopCard } from '@/app/(home)/_components/TopCard' + +import { tv } from '@/lib/tailwind/variants' + +const src = + 'https://res.cloudinary.com/trpfrog/image/upload/f_auto,c_limit,w_100,q_auto/environment/desk' + +const styles = tv({ + slots: { + bgImage: ['tw-bg-cover tw-bg-center tw-bg-no-repeat'], + base: [ + 'tw-h-full tw-w-full tw-p-8 tw-backdrop-blur-[2px]', + 'tw-bg-gradient-to-br tw-from-window-color tw-via-transparent tw-to-window-color', + 'tw-flex tw-flex-col tw-items-center tw-justify-center', + 'tw-text-center tw-text-4xl tw-font-bold tw-text-white tw-text-opacity-80', + ], + nameWrapper: 'tw-flex tw-items-baseline tw-gap-2 ', + name: 'first:tw-text-4xl first:tw-font-bold last:tw-text-2xl', + }, +})() + +export function EnvironmentCard() { + return ( + +
作業環境
+
+ ) +} diff --git a/src/app/(home)/_cards/FavoritesCard.tsx b/src/app/(home)/_cards/FavoritesCard.tsx new file mode 100644 index 00000000..bca4dbc0 --- /dev/null +++ b/src/app/(home)/_cards/FavoritesCard.tsx @@ -0,0 +1,80 @@ +import React from 'react' + +import { faCode, faStar, faWalking } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import { TopCard } from '@/app/(home)/_components/TopCard' + +import { tv } from '@/lib/tailwind/variants' +import { ParseWithBudouX } from '@/lib/wordSplit' + +const styles = tv({ + slots: { + grid: 'tw-grid tw-h-full tw-grid-cols-3 tw-gap-0.5', + wrapper: [ + 'tw-grid-rows tw-grid tw-grid-rows-subgrid', + 'tw-gap-1 tw-bg-zinc-200 tw-py-3', + '*:tw-text-center', + ], + icon: [ + 'tw-grid tw-place-items-center tw-text-4xl tw-font-bold', + 'tw-relative tw-top-1.5 tw-drop-shadow', + ], + title: 'tw-text-sm tw-font-bold tw-drop-shadow-sm', + description: 'tw-text-balance tw-text-center tw-text-[10px]', + }, +})() + +type FavoriteProps = { + icon: React.ReactNode + title: string + description?: string + className?: string + style?: React.CSSProperties +} + +function Favorite(props: FavoriteProps) { + return ( +
+
{props.icon}
+
{props.title}
+ {props.description && ( +
+ +
+ )} +
+ ) +} + +export function FavoritesCard() { + return ( + + } + className="tw-bg-gradient-to-br tw-from-sky-500 tw-to-sky-600 tw-text-white" + /> + } + description="裏道探すのとか好き/地図もよく見る" + className="tw-bg-gradient-to-br tw-from-pink-500 tw-to-pink-600 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-yellow-500 tw-to-yellow-600 tw-text-white" + /> + + ) +} diff --git a/src/app/(home)/_cards/MusicCard.tsx b/src/app/(home)/_cards/MusicCard.tsx new file mode 100644 index 00000000..fbc4f081 --- /dev/null +++ b/src/app/(home)/_cards/MusicCard.tsx @@ -0,0 +1,42 @@ +import { LinkTopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +import { tv } from '@/lib/tailwind/variants' + +const src = + 'https://res.cloudinary.com/trpfrog/image/upload/w_1000/musicbanner-with-notext.png' + +const styles = tv({ + slots: { + bg: [ + 'tw-bg-cover tw-bg-left tw-bg-no-repeat tw-py-3 tw-pl-[140px] tw-pr-5 tw-text-right', + 'tw-flex tw-flex-col tw-items-end tw-justify-center', + ], + h2: 'tw-text-2xl tw-font-bold tw-text-white', + text: 'tw-my-2 tw-text-[12px] tw-leading-tight tw-text-white *:tw-inline-block', + button: cardButtonStyle({ + class: 'tw-relative tw-bottom-0 tw-right-[-1px]', + }), + }, +})() + +export function MusicCard() { + return ( + +

Music

+
+ 友人の + ねぎ一世さんに + 作曲して + いただいた + テーマソング (?) +
+
+ ) +} diff --git a/src/app/(home)/_cards/SkillCard.tsx b/src/app/(home)/_cards/SkillCard.tsx new file mode 100644 index 00000000..4ca12896 --- /dev/null +++ b/src/app/(home)/_cards/SkillCard.tsx @@ -0,0 +1,111 @@ +import React from 'react' + +import { TopCard } from '@/app/(home)/_components/TopCard' + +import { Devicon } from '@/components/atoms/Devicon' + +import { tv } from '@/lib/tailwind/variants' +import { ParseWithBudouX } from '@/lib/wordSplit' + +const styles = tv({ + slots: { + grid: [ + 'tw-grid tw-h-full tw-gap-0.5 pc:tw-grid-cols-6', + 'sp:tw-flex sp:tw-flex-col', + ], + wrapper: [ + 'tw-grid-rows tw-grid tw-grid-rows-subgrid', + 'tw-gap-1 tw-bg-zinc-200 tw-py-3', + '*:tw-text-center', + 'sp:tw-flex sp:tw-items-center sp:tw-gap-4 sp:tw-pl-6 sp:*:tw-text-left', + ], + icon: [ + 'tw-grid tw-place-items-center tw-text-[2.8rem] tw-font-bold', + 'tw-relative tw-top-1.5 tw-drop-shadow sp:tw-top-0', + ], + title: 'tw-text-sm tw-font-bold tw-drop-shadow-sm', + text: 'tw-grid-rows-subgrid', + description: 'tw-text-balance tw-text-[10px]', + }, +})() + +type FavoriteProps = { + icon: React.ReactNode + title: string + description: string | string[] + className?: string + style?: React.CSSProperties +} + +function Skill(props: FavoriteProps) { + const description = + typeof props.description === 'string' + ? [props.description] + : props.description + return ( +
+
{props.icon}
+
+
{props.title}
+
+ {description + .map(desc => ) + .flatMap((x, i) => + i !== description.length - 1 ? [x,
] : x, + )} +
+
+
+ ) +} + +export function SkillCard() { + return ( + + } + className="tw-bg-gradient-to-br tw-from-blue-500 tw-to-blue-600 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-cyan-500 tw-to-cyan-600 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-amber-500 tw-to-amber-600 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-lime-600 tw-to-lime-700 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-violet-500 tw-to-violet-600 tw-text-white" + /> + } + className="tw-bg-gradient-to-br tw-from-pink-500 tw-to-pink-600 tw-text-white" + /> + + ) +} diff --git a/src/app/(home)/_cards/SocialCards.tsx b/src/app/(home)/_cards/SocialCards.tsx new file mode 100644 index 00000000..dbd90c8a --- /dev/null +++ b/src/app/(home)/_cards/SocialCards.tsx @@ -0,0 +1,77 @@ +import { faGithub, faTwitter } from '@fortawesome/free-brands-svg-icons' +import { faEnvelope } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { tv } from 'tailwind-variants' + +import { LinkTopCard } from '@/app/(home)/_components/TopCard' + +const styles = tv({ + slots: { + card: [ + 'tw-flex tw-items-center tw-justify-center', + 'tw-bg-gradient-to-br tw-p-2 tw-text-2xl tw-text-white', + ], + wrapper: 'tw-flex tw-flex-col tw-items-center tw-gap-1', + logo: 'tw-text-4xl !tw-leading-none tw-drop-shadow', + contact: + 'tw-text-lg tw-font-bold !tw-leading-none tw-drop-shadow-sm sp:tw-text-base', + }, +})() + +export function TwitterCard() { + return ( + +
+ + @TrpFrog +
+
+ ) +} + +export function GitHubCard() { + return ( + +
+ + trpfrog +
+
+ ) +} + +export function MailCard() { + return ( + +
+ +
+ contact + ★trpfrog.net +
+
+
+ ) +} diff --git a/src/app/(home)/_cards/StickersCard.tsx b/src/app/(home)/_cards/StickersCard.tsx new file mode 100644 index 00000000..d0dd5262 --- /dev/null +++ b/src/app/(home)/_cards/StickersCard.tsx @@ -0,0 +1,44 @@ +import { LinkTopCard } from '@/app/(home)/_components/TopCard' + +import { twMerge } from '@/lib/tailwind/merge' +import { tv } from '@/lib/tailwind/variants' + +const src = 'https://res.cloudinary.com/trpfrog/w_300/stickers' + +const styles = tv({ + slots: { + card: 'sp:tw-h-32', + absolute: 'tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-full', + bgImage: ` + tw-absolute tw-left-0 tw-top-0 + tw-h-full tw-w-full + -tw-skew-x-12 tw-skew-y-6 tw-scale-125 + tw-bg-cover tw-bg-center tw-bg-no-repeat + `, + text: ` + tw-flex tw-h-full tw-w-full tw-flex-col + tw-items-center tw-justify-center tw-bg-black/60 + tw-text-center tw-text-white + `, + }, +})() + +export function StickersCard() { + return ( + +
+

Stickers

+ LINE スタンプ 全2種 販売中 ! +
+
+ + ) +} diff --git a/src/app/(home)/_cards/StoreCard.tsx b/src/app/(home)/_cards/StoreCard.tsx new file mode 100644 index 00000000..114801a2 --- /dev/null +++ b/src/app/(home)/_cards/StoreCard.tsx @@ -0,0 +1,53 @@ +import { LinkTopCard } from '@/app/(home)/_components/TopCard' + +import { twMerge } from '@/lib/tailwind/merge' +import { tv } from '@/lib/tailwind/variants' + +const src = + 'https://res.cloudinary.com/trpfrog/image/upload/w_300/trpfrog-store.jpg' + +const styles = tv({ + slots: { + card: 'sp:tw-h-32', + absolute: 'tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-full', + bgImage: ` + tw-absolute tw-left-0 tw-top-0 + tw-h-full tw-w-full + -tw-skew-x-12 tw-skew-y-6 tw-scale-110 + tw-bg-cover tw-bg-center tw-bg-no-repeat + `, + text: ` + tw-flex tw-h-full tw-w-full tw-flex-col + tw-items-center tw-justify-center tw-bg-black/60 + tw-text-center tw-text-white + `, + }, +})() + +export function StoreCard() { + return ( + +
+

Goods

+ + つまみアイコン関連グッズ +
+ SUZURI にて販売中 ! +
+
+
+ + ) +} diff --git a/src/app/(home)/_cards/WalkingCard.tsx b/src/app/(home)/_cards/WalkingCard.tsx new file mode 100644 index 00000000..ed585236 --- /dev/null +++ b/src/app/(home)/_cards/WalkingCard.tsx @@ -0,0 +1,42 @@ +import { tv } from 'tailwind-variants' + +import { LinkTopCard } from '@/app/(home)/_components/TopCard' +import { cardButtonStyle } from '@/app/(home)/_styles/cardButtonStyle' + +const backgroundImage = + 'https://res.cloudinary.com/trpfrog/image/upload/w_600/blog/tokyotower-walking/20210324231643' + +const styles = tv({ + slots: { + bg: 'tw-bg-cover tw-bg-center tw-bg-no-repeat sp:tw-h-32', + wrapper: [ + 'tw-flex tw-flex-col tw-items-center tw-justify-between', + 'tw-bg-gradient-to-b tw-from-zinc-800 tw-via-transparent tw-to-zinc-800', + 'tw-h-full tw-w-full tw-p-3', + 'tw-text-center tw-text-xs tw-text-white tw-opacity-90', + ], + h2: 'tw-mb-1 tw-text-3xl tw-font-bold', + button: cardButtonStyle({ + className: 'tw-absolute tw-bottom-3 tw-right-4', + }), + }, +})() + +export function WalkingCard() { + return ( + +
+

Walking

+
+ たまに長距離を歩いて移動して +
+ 記事を書いています。(最長 70.5km) +
+
+
+ ) +} diff --git a/src/app/(home)/_cards/WorksCard.tsx b/src/app/(home)/_cards/WorksCard.tsx new file mode 100644 index 00000000..83813bf6 --- /dev/null +++ b/src/app/(home)/_cards/WorksCard.tsx @@ -0,0 +1,83 @@ +import path from 'path' + +import { LinkTopCard } from '@/app/(home)/_components/TopCard' +import { WorksFrontmatter, WorksFrontmatterSchema } from '@/app/works/schema' + +import { Devicon, hasDevicon } from '@/components/atoms/Devicon' + +import { MarkdownWithFrontmatter, readMarkdowns } from '@/lib/mdLoader' +import { tv } from '@/lib/tailwind/variants' +import { ParseWithBudouX } from '@/lib/wordSplit' + +const workStyles = tv({ + slots: { + wrapper: 'tw-rounded-sm tw-bg-cover tw-bg-center', + backdrop: + 'tw-size-full tw-p-2 tw-backdrop-blur-[1.5px] ' + + 'tw-flex tw-h-full tw-w-full tw-flex-col tw-justify-end tw-text-white', + title: + 'tw-text-lg tw-font-bold tw-leading-tight tw-text-white tw-drop-shadow', + subtitle: + 'tw-mt-1 tw-text-[11px] tw-leading-tight tw-text-white tw-drop-shadow', + keywords: + 'tw-flex tw-gap-1 tw-text-xs tw-leading-none ' + + 'tw-mb-1 tw-w-fit tw-overflow-hidden', + keyword: 'tw-text-lg tw-drop-shadow', + }, +})() + +function Work(props: { + content: MarkdownWithFrontmatter + className?: string +}) { + const { content, className } = props + const imageUrl = `https://res.cloudinary.com/trpfrog/image/upload/w_150/${content.metadata.image?.path}` + + return ( +
+
+
+ {content.metadata.keywords + ?.filter(hasDevicon) + .map(k => ( + + ))} +
+

+ +

+ {content.metadata.subtitle && ( +
+ {content.metadata.subtitle} +
+ )} +
+
+ ) +} + +const styles = tv({ + slots: { + wrapper: 'tw-grid tw-grid-cols-3 tw-gap-0.5 sp:tw-grid-cols-2', + }, +})() + +export async function WorksCard() { + const contents = await readMarkdowns( + path.join(process.cwd(), 'src', 'app', 'works', 'contents'), + WorksFrontmatterSchema, + ).then(mds => mds.slice(0, 3)) + + return ( + + + + + + ) +} diff --git a/src/app/(home)/_cards/index.ts b/src/app/(home)/_cards/index.ts new file mode 100644 index 00000000..2d03e665 --- /dev/null +++ b/src/app/(home)/_cards/index.ts @@ -0,0 +1,16 @@ +export { AboutMeCard } from './AboutMeCard' +export { AICard } from './AICard' +export { BalloonCard } from './BalloonCard/BalloonCard' +export { BelongingCard } from './BelongingCard' +export { BirdsCard } from './BirdsCard' +export { WorksCard } from './WorksCard' +export { BlogCard } from './BlogCard' +export { CompetitiveCard } from './CompetitiveCard' +export { EnvironmentCard } from './EnvironmentCard' +export { FavoritesCard } from './FavoritesCard' +export { MusicCard } from './MusicCard' +export { SkillCard } from './SkillCard' +export { StickersCard } from './StickersCard' +export { StoreCard } from './StoreCard' +export { TwitterCard, GitHubCard, MailCard } from './SocialCards' +export { WalkingCard } from './WalkingCard' diff --git a/src/app/(home)/_components/AboutMe/index.module.scss b/src/app/(home)/_components/AboutMe/index.module.scss index 27ab5b09..2bf1cad0 100644 --- a/src/app/(home)/_components/AboutMe/index.module.scss +++ b/src/app/(home)/_components/AboutMe/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; .intro_text { margin: 0.7em 0.3em 0; diff --git a/src/app/(home)/_components/AboutMe/index.tsx b/src/app/(home)/_components/AboutMe/index.tsx index be7e08c2..fb518937 100644 --- a/src/app/(home)/_components/AboutMe/index.tsx +++ b/src/app/(home)/_components/AboutMe/index.tsx @@ -1,85 +1,26 @@ -import { faGithub, faTwitter } from '@fortawesome/free-brands-svg-icons' -import { faAt, faEnvelope, faHeart } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { tv } from 'tailwind-variants' -import { - additionalAttributes, - attributes, -} from '@/app/(home)/_components/AboutMe/attributes' -import { KCommandBox } from '@/app/(home)/_components/AboutMe/KCommandBox' - -import { Block } from '@/components/molecules/Block' - -import styles from './index.module.scss' +const createStyles = tv({ + slots: { + wrapper: 'tw-grid tw-grid-cols-subgrid sp:tw-grid-cols-3', + aboutMe: + 'tw-bg-amber-300 sp:tw-col-span-3 pc:tw-col-span-2 pc:tw-row-span-3', + social: 'tw-bg-amber-300', + }, +}) type Props = { id: string } -export const AboutMe = ({ id }: Props) => { +export function AboutMe() { + const styles = createStyles() return ( - -
-

- つまみ - TrpFrog -

-
- -

ふにゃ〜

- -
    - {attributes.map(({ icon, iconName, text }) => ( -
  • - - - - {text} -
  • - ))} -
- -

- {"Otaku's favorite"} -

-
    - {additionalAttributes.map(({ icon, text, iconName }) => ( -
  • - - - - - {iconName} -
    - {text} -
    -
  • - ))} -
-
- -
-
- {' '} - TrpFrog -
-
- {' '} - TrpFrog -
-
- dev - - trpfrog.net -
-
-
+
+
AboutMe
+
GitHub
+
Twitter
+
Mail
+
) } diff --git a/src/app/(home)/_components/Bird.tsx b/src/app/(home)/_components/Bird.tsx deleted file mode 100644 index 00bffd58..00000000 --- a/src/app/(home)/_components/Bird.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import styles from '@/app/(home)/page.module.scss' - -import { Block } from '@/components/molecules/Block' - -type Props = { - id?: string -} - -export const Bird = ({ id }: Props) => { - const birdX = [ - '', - '   / ̄ ̄\ ムシャムシャ', - ' /  (●)/ ̄ ̄\', - '.  /    ト、   \', - ' 彳     \\  |', - './   /⌒ヽヽ  |', - '/      |  | .|  /。', - '    |  ヽ|/∴', - '       。゜', - '', - '', - ].join('\n') - - const birdY = [ - 'オエーー !!! ___', - '    ___/   ヽ', - '   /   / /⌒ヽ|', - '/ (゚)/  / /', - '.  /    ト、 /。⌒ヽ。', - ' 彳    \\゚。∴。o', - './     \\。゚。o', - '/     /⌒\U∴)', - '      |   ゙U |', - '      |    | |', - '        U', - ].join('\n') - - return ( - -
-
{birdX}
-
-          {birdY}
-        
-
-
- ) -} diff --git a/src/app/(home)/_components/Links/index.tsx b/src/app/(home)/_components/Links/index.tsx deleted file mode 100644 index 752ef148..00000000 --- a/src/app/(home)/_components/Links/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import styles from '@/app/(home)/page.module.scss' - -import { Button } from '@/components/atoms/Button' -import { H2 } from '@/components/atoms/H2' -import { Block } from '@/components/molecules/Block' - -import links from './links.json' - -export type MyLinkRecord = { - url: string - siteName: string - description: string -} - -type Props = { - id?: string -} - -export async function Links({ id }: Props) { - return ( - -
- {(links as MyLinkRecord[]).map(({ url, siteName, description }) => ( -
-

- -

-

{description}

-
- ))} -
- -

相互リンク

-

移動しました!

-

- -

-
- ) -} diff --git a/src/app/(home)/_components/Links/links.json b/src/app/(home)/_components/Links/links.json deleted file mode 100644 index 402ecfdf..00000000 --- a/src/app/(home)/_components/Links/links.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "url": "https://twitter.com/TrpFrog", - "siteName": "Twitter", - "description": "割とうるさいので注意" - }, - { - "url": "https://marshmallow-qa.com/trpfrog", - "siteName": "マシュマロ", - "description": "見てなすぎて無視することがあり、すみません" - }, - { - "url": "https://www.amazon.jp/hz/wishlist/ls/2MHXSEDPHJ79J?ref_=wl_share", - "siteName": "欲しいものリスト", - "description": "なんか送ってくれると喜びます" - }, - { - "url": "https://line.me/S/sticker/4674940", - "siteName": "LINEスタンプ", - "description": "好評発売中!(120円くらい)" - }, - { - "url": "https://trpfrog.hateblo.jp", - "siteName": "はてなブログ", - "description": "技術、徒歩会の話とか" - }, - { - "url": "https://github.com/TrpFrog", - "siteName": "GitHub", - "description": "様々な制作物のソースコード" - } -] \ No newline at end of file diff --git a/src/app/(home)/_components/Ratings/index.module.scss b/src/app/(home)/_components/Ratings/index.module.scss deleted file mode 100644 index 1d673a2a..00000000 --- a/src/app/(home)/_components/Ratings/index.module.scss +++ /dev/null @@ -1,102 +0,0 @@ -@use '@/styles/mixins'; - -.rating_list { - font-weight: bold; -} - -.platinum { - /*filter:drop-shadow(1px 1px 0.6pt rgba(0, 0, 0,30));*/ - color: #ff7c00; - background: -webkit-linear-gradient( - top, - #fff677 30%, - #fffbb9 50%, - #ffcb5d 50%, - #fff4b3 80% - ); - -webkit-text-fill-color: rgba(255, 255, 255, 0); - /*text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);*/ - -webkit-background-clip: text; - -webkit-text-stroke: 1px #533c15; -} - -.rainbow { - /*filter:drop-shadow(1px 1px 0.6pt rgba(0, 0, 0,30));*/ - color: #fff0af; - background: -webkit-linear-gradient( - top, - red 15%, - orange, - yellow, - #00d907 60%, - aqua, - #002fff, - #f500f5 - ); - - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-background-clip: text; - -webkit-text-stroke: 1px #858585; - /*text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);*/ -} - -.silver { - /*filter:drop-shadow(1px 1px 0.6pt rgba(0, 0, 0,30));*/ - background: -webkit-linear-gradient( - top, - #fffdfd 30%, - #cbc9c9 50%, - #aeacac 51%, - #828080 80% - ); - -webkit-background-clip: text; - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-text-stroke: 1px #3d3d3d; - /*text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);*/ -} - -.brown { - background: -webkit-linear-gradient(top, #ffa200 25%, #c64000 75%); - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-background-clip: text; - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); -} - -.green { - background: -webkit-linear-gradient(top, #47c017 25%, #006609 75%); - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-background-clip: text; - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); -} - -.water { - background: -webkit-linear-gradient(top, #42c5f8 25%, #0a9cd6 75%); - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-background-clip: text; - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); -} - -.blue { - background: -webkit-linear-gradient(top, #2495ff 25%, #072792 75%); - -webkit-text-fill-color: rgba(255, 255, 255, 0); - -webkit-background-clip: text; - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); -} - -@include mixins.mq(dark) { - .platinum, - .rainbow, - .silver, - .brown, - .green, - .water, - .blue { - -webkit-text-stroke: 2px white; - text-stroke: 2px white; - filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.438)); - } -} - -ul.rating_list li { - margin-bottom: 0.5em; -} diff --git a/src/app/(home)/_components/Ratings/index.tsx b/src/app/(home)/_components/Ratings/index.tsx deleted file mode 100644 index 5c43ec03..00000000 --- a/src/app/(home)/_components/Ratings/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Block } from '@/components/molecules/Block' - -import styles from './index.module.scss' - -type Props = { - id?: string -} - -export const Ratings = ({ id }: Props) => { - return ( - -
    -
  • - AtCoder (TrpFrog - )
    - - highest - - - 1596 - -
  • -
  • - Codeforces ( - TrpFrog)
    - - max - - - 1687 - -
  • -
-
- ) -} diff --git a/src/app/(home)/_components/Store.tsx b/src/app/(home)/_components/Store.tsx deleted file mode 100644 index e922bfc6..00000000 --- a/src/app/(home)/_components/Store.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Image from 'next/legacy/image' - -import styles from '@/app/(home)/page.module.scss' - -import { Button } from '@/components/atoms/Button' -import { Block } from '@/components/molecules/Block' - -type Props = { - id?: string -} - -export const Store = ({ id }: Props) => { - return ( - -

- つまみさんのスタンプ・グッズ -
- 好評発売中! -

-
-
- -
-
- {'つまみグッズの画像'} - -
-
- {'つまみグッズの画像'} - -
-
-
- ) -} diff --git a/src/app/(home)/_components/TempTwitter/index.module.scss b/src/app/(home)/_components/TempTwitter/index.module.scss index ac92ca26..ae3a6378 100644 --- a/src/app/(home)/_components/TempTwitter/index.module.scss +++ b/src/app/(home)/_components/TempTwitter/index.module.scss @@ -12,7 +12,7 @@ white-space: nowrap; display: inline-block; color: var(--footer-color); - font-family: var(--font-roboto); + font-family: var(--font-inter); -webkit-text-stroke: 1px var(--footer-color); text-stroke: 1px var(--footer-color); transform: translateY(1px); diff --git a/src/app/(home)/_components/TempTwitter/index.tsx b/src/app/(home)/_components/TempTwitter/index.tsx index e6549aae..87e62973 100644 --- a/src/app/(home)/_components/TempTwitter/index.tsx +++ b/src/app/(home)/_components/TempTwitter/index.tsx @@ -1,17 +1,17 @@ import { Fragment, Suspense } from 'react' import matter from 'gray-matter' -import ReactMarkdown from 'react-markdown' -import rehypeRaw from 'rehype-raw' -import remarkGfm from 'remark-gfm' +import { MDXRemote } from 'next-mdx-remote/rsc' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { LoadingBlock } from '@/components/molecules/LoadingBlock' +import { HorizontalRule } from '@/components/wrappers/HorizontalRule' import { microCMS } from '@/lib/microCMS' import { ShowAllComponent } from '@blog/_components/article-parts/ShowAll/ShowAllComponent' +import { getMarkdownOptions } from '@blog/_renderer/rendererProperties' import styles from './index.module.scss' @@ -52,15 +52,10 @@ export async function TempTwitter() { {date}
- <>{children}, - }} - > - {content} - +
) @@ -69,10 +64,10 @@ export async function TempTwitter() { const maxTweetsDisplayedAtOnce = 5 return ( - +

なんらかの原因でツイートできなくなったときに逃げてくる場所です。

-
+ }> {tweets.length > maxTweetsDisplayedAtOnce ? ( { + title?: string + titlePosition?: VariantProps['position'] +} + +export function TopCard(props: TopCardProps) { + const { + className, + children, + title, + titlePosition = 'top-left', + ...rest + } = props + return ( +
+ {title && ( +

+ {title} +

+ )} + {children} +
+ ) +} + +export interface TopLinkCardProps extends AProps { + title?: string + readMoreText?: string | boolean + titlePosition?: VariantProps['position'] +} + +export function LinkTopCard(props: TopLinkCardProps) { + const { + className, + children, + title, + readMoreText, + titlePosition = 'top-left', + ...rest + } = props + const showReadMore = readMoreText !== false + const readMore = typeof readMoreText === 'string' ? readMoreText : '→' + + return ( + + {title && ( +

+ {title} +

+ )} + {children} + {showReadMore &&
{readMore}
} +
+ ) +} diff --git a/src/app/(home)/_components/TopPageBalloons.tsx b/src/app/(home)/_components/TopPageBalloons.tsx deleted file mode 100644 index 8cec5639..00000000 --- a/src/app/(home)/_components/TopPageBalloons.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import styles from '@/app/(home)/page.module.scss' -import { Balloon } from '@/app/balloon/_components/Balloon' -import { useBalloonState } from '@/app/balloon/_components/BalloonArray' - -import { Button } from '@/components/atoms/Button' -import { Block } from '@/components/molecules/Block' - -type Props = { - id?: string -} - -export const TopPageBalloons = ({ id }: Props) => { - const balloonAmount = 7 - const { isBurst, balloonColorArray, onBurst } = useBalloonState( - balloonAmount, - id ?? '', - ) - - return ( - -
- {Array.from(Array(7), (v, k) => ( - - onBurst({ - index: k, - currentAmount: balloonAmount, - }) - } - /> - ))} -
-

- -

-
- ) -} diff --git a/src/app/(home)/_components/TopPageIcons.tsx b/src/app/(home)/_components/TopPageIcons.tsx deleted file mode 100644 index b24244e1..00000000 --- a/src/app/(home)/_components/TopPageIcons.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Image from 'next/legacy/image' - -import styles from '@/app/(home)/page.module.scss' - -import { Button } from '@/components/atoms/Button' -import { Block } from '@/components/molecules/Block' - -type Props = { - id?: string -} - -export const TopPageIcons = ({ id }: Props) => { - return ( - -
- {[0, 7, 5, 6] - .map(i => i.toString()) - .map(i => ( - {i - ))} -
- -
- ) -} diff --git a/src/app/(home)/_components/TopPageMusic.tsx b/src/app/(home)/_components/TopPageMusic.tsx deleted file mode 100644 index bd11052c..00000000 --- a/src/app/(home)/_components/TopPageMusic.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' - -import Image from 'next/legacy/image' -import LiteYouTubeEmbed from 'react-lite-youtube-embed' - -import { Button } from '@/components/atoms/Button' -import { Block } from '@/components/molecules/Block' - -type Props = { - id?: string -} - -export const TopPageMusic = ({ id }: Props) => { - return ( - -

- ねぎ一世(@negiissei)さんに「 - つまみのうた」を作っていただきました!(????) - ありがとうございます!!! -

-
- -
-

- Apple Music, Spotify, YouTube Music, LINE Music 他 - 各種サイトで配信中!(なんで?) -

-

- {'つまみのうたのバナー'} -

-

- - -

-
- ) -} diff --git a/src/app/(home)/_components/TrpFrogIconFrame/Description.tsx b/src/app/(home)/_components/TrpFrogIconFrame/Description.tsx deleted file mode 100644 index 48d4eae3..00000000 --- a/src/app/(home)/_components/TrpFrogIconFrame/Description.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { get } from '@vercel/edge-config' - -import { - TRPFROG_DIFFUSION_DEFAULT_UPDATE_HOURS, - TRPFROG_DIFFUSION_UPDATE_HOURS_EDGE_CONFIG_KEY, -} from '@/lib/constants' - -export async function Description() { - const updateHours = - (await get(TRPFROG_DIFFUSION_UPDATE_HOURS_EDGE_CONFIG_KEY)) ?? - TRPFROG_DIFFUSION_DEFAULT_UPDATE_HOURS - - return ( - -
- これは AI により自動生成された画像です。 -
- 最後の生成から{updateHours} - 時間経つと再生成されます。 -
-
- Powered by{' '} - - Prgckwb/trpfrog-diffusion - -
-
- ) -} diff --git a/src/app/(home)/_components/TrpFrogIconFrame/IconFrame.tsx b/src/app/(home)/_components/TrpFrogIconFrame/IconFrame.tsx deleted file mode 100644 index 68c62f6f..00000000 --- a/src/app/(home)/_components/TrpFrogIconFrame/IconFrame.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' -import Balancer from 'react-wrap-balancer' -import useSWR, { Fetcher } from 'swr' - -import styles from '@/app/(home)/_components/TrpFrogIconFrame/index.module.scss' -import type { TrpFrogImageGenerationResult } from '@/app/api/trpfrog-diffusion/route' - -import { WaveText } from '@/components/atoms/WaveText' - -import { ParseWithBudouX } from '@/lib/wordSplit' - -export function IconFrame() { - const fetcher: Fetcher = url => - fetch(url).then(r => r.json()) - - const { data, error, isLoading } = useSWR('/api/trpfrog-diffusion', fetcher) - - if (isLoading) { - return ( -
-
- Loading... -
-
-
-
-
-
- ) - } - - if (error || !data || !data.base64) { - return ( -
-
- Error occurred -
-
-

- エラーが発生しました。 -
- 時間をあけて再度お試しください。 -

-
-
- ) - } - - const { base64, prompt, translated } = data - return ( -
- {`Auto -
-
- {prompt} -
-
- - - -
-
-
- ) -} diff --git a/src/app/(home)/_components/TrpFrogIconFrame/index.module.scss b/src/app/(home)/_components/TrpFrogIconFrame/index.module.scss deleted file mode 100644 index fba694a1..00000000 --- a/src/app/(home)/_components/TrpFrogIconFrame/index.module.scss +++ /dev/null @@ -1,74 +0,0 @@ -//const style = { -// width: props.size ?? '100%', -// aspectRatio: '1 / 1', -// borderRadius: '1rem', -// background: 'gray', -// display: 'flex', -// justifyContent: 'center', -// alignItems: 'center', -// fontSize: '2rem', -// color: 'white', -//} - -.picture { - width: 100%; - aspect-ratio: 1 / 1; - border-radius: 1rem; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); -} - -.loading_picture { - @extend .picture; - background: lightgray; - font-family: var(--font-m-plus-rounded-1c); - font-weight: bold; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.5rem; -} - -.error_picture { - @extend .loading_picture; - background: #d25454; - color: white; -} - -.error_caption { - font-family: var(--font-noto-sans-jp); - font-size: 1rem; -} - -.caption_en { - font-size: 1.2em; - font-weight: bold; - font-style: italic; - margin: 10px 0; -} - -.waiting.caption_en { - margin: 20px 0 0 0; - display: inline-block; - width: 50%; - height: 1em; - background: gray; - border-radius: 3px; -} - -.caption_ja { - font-size: 0.8em; -} - -.waiting.caption_ja { - display: inline-block; - margin-top: 0; - width: 80%; - height: 1em; - background: gray; - border-radius: 3px; -} - -.caption_wrapper { - text-align: center; - line-height: 1.2; -} diff --git a/src/app/(home)/_components/TrpFrogIconFrame/index.tsx b/src/app/(home)/_components/TrpFrogIconFrame/index.tsx deleted file mode 100644 index ef74bb53..00000000 --- a/src/app/(home)/_components/TrpFrogIconFrame/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Suspense } from 'react' -import * as React from 'react' - -import { WaveText } from '@/components/atoms/WaveText' -import { Block } from '@/components/molecules/Block' - -import { Description } from './Description' -import { IconFrame } from './IconFrame' - -type Props = React.ComponentPropsWithoutRef<'div'> - -export function TrpFrogIconFrame(props: Props) { - return ( - - -
- - Loading... -

- } - > - -
-
- ) -} diff --git a/src/app/(home)/_components/WhatsNew/index.module.scss b/src/app/(home)/_components/WhatsNew/index.module.scss deleted file mode 100644 index 654c67f2..00000000 --- a/src/app/(home)/_components/WhatsNew/index.module.scss +++ /dev/null @@ -1,58 +0,0 @@ -@use '@/styles/mixins'; - -.block { - display: flex; - flex-direction: column; - padding: var(--window-padding-top-bottom) - min(var(--window-padding-left-right), 1.5em); -} - -.table_wrapper { - position: relative; - height: 100%; - min-height: 350px; - flex: 1; -} - -#whats_new_table { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - - font-size: 0.8em; - line-height: 1.2em; - width: 100%; - margin-top: 10px; -} - -.whats_new_row { - min-height: 2.5em; - display: grid; - grid-template-columns: 3em auto; - border-bottom: dashed 2px var(--window-bottom-color); - padding: 5px; - - @include mixins.mq(dark) { - border-color: white; - } - - div { - display: flex; - align-items: center; - } - - div + div { - margin-left: 10px; - } - - p { - margin: 0; - } -} - -.whats_new_date { - width: 3.5em; - margin-right: 5px; -} diff --git a/src/app/(home)/_components/WhatsNew/index.tsx b/src/app/(home)/_components/WhatsNew/index.tsx deleted file mode 100644 index 9489e122..00000000 --- a/src/app/(home)/_components/WhatsNew/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import dayjs from 'dayjs' -import ReactMarkdown from 'react-markdown' - -import { HoverScrollBox } from '@/components/atoms/HoverScrollBox' -import { Block } from '@/components/molecules/Block' - -import { retrieveSortedBlogPostList } from '@blog/_lib/load' - -import styles from './index.module.scss' - -type Props = { - id?: string -} - -type WhatsNewRecord = { - type: 'page' | 'fix' | 'improve' | 'blog' | 'content' - text: string - date: string -} - -const getWhatsNewRecords: () => Promise = async () => { - const jsonPath = path.join(process.cwd(), 'src', 'data', 'whats_new.json') - const jsonText = fs.readFileSync(jsonPath, 'utf-8') - const records = JSON.parse(jsonText) as WhatsNewRecord[] - - const blogData = await retrieveSortedBlogPostList() - - for (const post of blogData) { - records.push({ - type: 'blog', - text: `記事「[${post.title}](https://trpfrog.net/blog/${post.slug})」を公開しました!`, - date: dayjs(post.date).format('YYYY-MM-DD'), - }) - } - - return records.sort((a, b) => { - if (a.date === b.date) return 0 - return a.date < b.date ? 1 : -1 - }) -} - -export async function WhatsNew({ id }: Props) { - const whatsNewRecords: WhatsNewRecord[] = await getWhatsNewRecords() - return ( - -
- - {whatsNewRecords.map(({ text, date }) => { - const [y, m, d] = date.split('-') - if (process.env.NODE_ENV !== 'production') { - const localhost = 'http://localhost:3000' - text = text.replace(/https:\/\/trpfrog.net/g, localhost) - } - return ( -
-
- {y}-
- {m}-{d} -
-
- {text} -
-
- ) - })} -
-
-
- ) -} diff --git a/src/app/(home)/_styles/cardButtonStyle.ts b/src/app/(home)/_styles/cardButtonStyle.ts new file mode 100644 index 00000000..cc0a460c --- /dev/null +++ b/src/app/(home)/_styles/cardButtonStyle.ts @@ -0,0 +1,20 @@ +import { tv } from 'tailwind-variants' + +export const cardButtonStyle = tv({ + base: [ + 'tw-inline-flex tw-w-fit tw-flex-row tw-items-center tw-justify-center', + 'tw-rounded-full tw-px-3 tw-py-1', + 'tw-whitespace-nowrap tw-text-sm tw-font-bold', + 'tw-backdrop-blur', + ], + variants: { + invertColor: { + true: 'tw-bg-text-color/80 tw-text-window-color hover:tw-bg-text-color', + false: + 'tw-bg-window-color/80 tw-text-text-color hover:tw-bg-window-color', + }, + }, + defaultVariants: { + invertColor: false, + }, +}) diff --git a/src/app/(home)/page.module.css b/src/app/(home)/page.module.css new file mode 100644 index 00000000..8f0f32a9 --- /dev/null +++ b/src/app/(home)/page.module.css @@ -0,0 +1,40 @@ +.layout { + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(3, 100px) repeat(7, 150px); + grid-template-areas: + 'about-me about-me twitter' + 'about-me about-me github' + 'about-me about-me mail' + 'univ blog blog' + 'favs blog blog' + 'works works walking' + 'ai env comp' + 'ai skills skills' + 'ai balloon music' + 'stickers store birds'; + + @media screen and (max-width: 799px) { + grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-template-rows: 350px 80px 300px 150px 150px 150px auto auto auto 520px repeat( + 6, + auto + ); + grid-template-areas: + 'about-me about-me about-me about-me about-me about-me' + 'twitter twitter github github mail mail' + 'blog blog blog blog blog blog' + 'works works works works works works' + 'univ univ univ univ univ univ' + 'favs favs favs favs favs favs' + 'walking walking walking walking walking walking' + 'skills skills skills skills skills skills' + 'comp comp comp comp comp comp' + 'ai ai ai ai ai ai' + 'balloon balloon balloon balloon balloon balloon' + 'env env env env env env' + 'music music music music music music' + 'stickers stickers stickers stickers stickers stickers' + 'store store store store store store' + 'birds birds birds birds birds birds'; + } +} diff --git a/src/app/(home)/page.module.scss b/src/app/(home)/page.module.scss deleted file mode 100644 index 74e62eea..00000000 --- a/src/app/(home)/page.module.scss +++ /dev/null @@ -1,141 +0,0 @@ -@use '@/styles/mixins'; - -#links { - table { - border-spacing: 1em; - } -} - -.link_grid { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - margin: 30px 0; -} - -.link_block { - display: inline-block; - background: rgba(120, 189, 0, 0.09); - padding: 10px 20px; - border-radius: 20px; - text-align: center; -} - -#store_links { - display: block; - - a { - width: 100%; - } -} - -#icons { - .top_icons { - margin: 10px 0; - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: 1fr; - } -} - -#bird { - .aa { - white-space: pre; - font-size: 45%; - line-height: 12px; - display: inline-block; - } -} - -@media screen and (max-width: 980px) { - #sticker table, - #sticker td, - #sticker th, - #sticker tbody { - display: block; - } -} - -#top_balloon_grid { - display: grid; - margin: 10px 0; - grid-template-columns: repeat(7, min(130px, 12vw)); - grid-template-rows: min(217px, 20vw); -} - -#top_page_grid { - max-width: 1000px; - display: inline-grid; - grid-template-areas: - 'about_me about_me whats_new' - 'about_me about_me whats_new' - 'sticker icons icons' - 'sticker music music' - 'bird music music' - 'balloon balloon balloon' - 'links links diffusion' - 'links links music_game'; - grid-template-columns: 3fr 2fr 3fr; - grid-gap: var(--main-margin); - text-align: left; -} - -@include mixins.mq(sp) { - #top_page_grid { - grid-template-areas: - 'about_me' - 'whats_new' - 'icons' - 'music' - 'sticker' - 'balloon' - 'diffusion' - 'links' - 'music_game' - 'bird' - 'stats'; - grid-template-columns: 1fr; - } -} - -// Grid-area definition - -#about_me_grid { - grid-area: about_me; -} - -#music_game { - grid-area: music_game; -} - -#links { - grid-area: links; -} - -#music { - grid-area: music; -} - -#icons { - grid-area: icons; -} - -#balloon { - grid-area: balloon; -} - -#sticker { - grid-area: sticker; -} - -#bird { - grid-area: bird; -} - -#whats_new { - grid-area: whats_new; -} - -#diffusion { - grid-area: diffusion; -} diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 5ad868fe..214f0f8d 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -1,38 +1,82 @@ import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css' -import { TrpFrogIconFrame } from '@/app/(home)/_components/TrpFrogIconFrame' +import { tv } from 'tailwind-variants' + +import * as cards from '@/app/(home)/_cards' import { MainWrapper } from '@/components/atoms/MainWrapper' -import { AboutMe } from './_components/AboutMe' -import { Bird } from './_components/Bird' -import { Links } from './_components/Links' -import { Ratings } from './_components/Ratings' -import { Store } from './_components/Store' import { TempTwitter } from './_components/TempTwitter' -import { TopPageBalloons } from './_components/TopPageBalloons' -import { TopPageIcons } from './_components/TopPageIcons' -import { TopPageMusic } from './_components/TopPageMusic' import { TrpFrogAnimation as TrpFrogAnimationFrame } from './_components/TrpFrogAnimation' -import { WhatsNew } from './_components/WhatsNew' -import styles from './page.module.scss' +import css from './page.module.css' + +const styles = tv({ + slots: { + grid: 'tw-grid tw-gap-3 sp:tw-gap-2', + subgrid: 'tw-col-span-full tw-grid tw-grid-cols-subgrid', + }, +})() -export default function Index() { +export default async function Index() { return ( <> -
- - - - - - - - - - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
diff --git a/src/app/api/trpfrog-diffusion/route.ts b/src/app/api/trpfrog-diffusion/route.ts index 39ddd701..3ce4760f 100644 --- a/src/app/api/trpfrog-diffusion/route.ts +++ b/src/app/api/trpfrog-diffusion/route.ts @@ -47,7 +47,7 @@ const cache = new LRUCache< async function getCache() { const key = TRPFROG_DIFFUSION_KV_KEY - let record = await kv.get(key) + const record = await kv.get(key) if (process.env.NODE_ENV === 'development') { return record as unknown as TrpFrogImageGenerationResult } diff --git a/src/app/balloon/BalloonApp.tsx b/src/app/balloon/BalloonApp.tsx index fd63b6c7..d641f8e5 100644 --- a/src/app/balloon/BalloonApp.tsx +++ b/src/app/balloon/BalloonApp.tsx @@ -2,12 +2,14 @@ import { useState } from 'react' -import { Button } from '@/components/atoms/Button' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' +import { Input } from '@/components/wrappers' import { clamp } from '@/lib/utils' +import { MagicButton } from 'src/components/atoms/MagicButton' + import { useBalloonSound } from './_components/Balloon' import { BalloonArray } from './_components/BalloonArray' @@ -40,14 +42,14 @@ export function BalloonApp() { description={'風船を割ることができます。(?)'} >

- + {' '}

diff --git a/src/app/balloon/page.tsx b/src/app/balloon/page.tsx index 3ec77bf3..0f97faf7 100644 --- a/src/app/balloon/page.tsx +++ b/src/app/balloon/page.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { export default async function Index() { return ( - + ) diff --git a/src/app/blog/[slug]/[[...options]]/_components/ArticleSidebar.tsx b/src/app/blog/[slug]/[[...options]]/_components/ArticleSidebar.tsx index 336983f2..18349ca3 100644 --- a/src/app/blog/[slug]/[[...options]]/_components/ArticleSidebar.tsx +++ b/src/app/blog/[slug]/[[...options]]/_components/ArticleSidebar.tsx @@ -1,5 +1,5 @@ import { Block } from '@/components/molecules/Block' -import { HeaderFollowSticky } from '@/components/organisms/Header' +import { StickToTop } from '@/components/organisms/Header' import { ArticleCard } from '@blog/_components/ArticleCard' import { PageNavigation } from '@blog/_components/PageNavigation' @@ -14,7 +14,7 @@ type Props = { export function ArticleSidebar({ post }: Props) { return ( - +
- + ) } diff --git a/src/app/blog/[slug]/[[...options]]/_components/EditButton.tsx b/src/app/blog/[slug]/[[...options]]/_components/EditButton.tsx index 3b2ef567..1591fcfc 100644 --- a/src/app/blog/[slug]/[[...options]]/_components/EditButton.tsx +++ b/src/app/blog/[slug]/[[...options]]/_components/EditButton.tsx @@ -1,11 +1,11 @@ 'use client' -import { Button } from '@/components/atoms/Button' - import { openInCotEditor } from '@blog/actions/openInCotEditor' +import { MagicButton } from 'src/components/atoms/MagicButton' + export function EditButton({ slug }: { slug: string }) { return process.env.NODE_ENV === 'development' ? ( - + openInCotEditor(slug)}>編集する ) : ( <> ) diff --git a/src/app/blog/[slug]/[[...options]]/_components/EntryButtons.tsx b/src/app/blog/[slug]/[[...options]]/_components/EntryButtons.tsx index 4d04b408..bbad5ab6 100644 --- a/src/app/blog/[slug]/[[...options]]/_components/EntryButtons.tsx +++ b/src/app/blog/[slug]/[[...options]]/_components/EntryButtons.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { faTwitter } from '@fortawesome/free-brands-svg-icons' import { faArrowLeft, faPencil } from '@fortawesome/free-solid-svg-icons' -import Link from 'next/link' -import { Button } from '@/components/atoms/Button' +import { A } from '@/components/wrappers' import { EntryButton } from '@blog/_components/EntryButton' import { TogglePageViewLink } from '@blog/_components/TogglePageViewLink' -import { UDFontButton } from '@blog/_components/UDFontBlock' import { BlogPost } from '@blog/_lib/blogPost' +import { MagicButton } from 'src/components/atoms/MagicButton' + import { ShareSpan } from './ShareSpan' type EntryButtonProps = Omit< @@ -25,20 +25,17 @@ export function RichEntryButtons(props: EntryButtonProps) { const { post, extended, ...rest } = props return (
- + - + - + - - {extended && ( - <> - - {post.numberOfPages >= 2 && } - + + {extended && post.numberOfPages >= 2 && ( + )}
) @@ -56,16 +53,16 @@ export function EntryButtons({ post, style, ...rest }: EntryButtonProps) { }} {...rest} > - + 記事一覧 - + ツイート - +
) } diff --git a/src/app/blog/[slug]/[[...options]]/page.tsx b/src/app/blog/[slug]/[[...options]]/page.tsx index 28900130..5bac6d62 100644 --- a/src/app/blog/[slug]/[[...options]]/page.tsx +++ b/src/app/blog/[slug]/[[...options]]/page.tsx @@ -1,12 +1,10 @@ import { Metadata } from 'next' -import { MainWrapper } from '@/components/atoms/MainWrapper' +import { gridLayoutStyle, MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { ArticleHeader } from '@blog/_components/ArticleHeader' -import { BadBlogBlock } from '@blog/_components/BadBlog' import { RelatedPosts } from '@blog/_components/RelatedPosts' -import { UDFontBlock } from '@blog/_components/UDFontBlock' import { BlogPost } from '@blog/_lib/blogPost' import { fetchBlogPost, retrieveSortedBlogPostList } from '@blog/_lib/load' import { BlogMarkdown } from '@blog/_renderer/BlogMarkdown' @@ -92,19 +90,15 @@ export default async function Index({ params: { slug, options } }: PageProps) { const initialNode = await renderBlog(slug, page) return ( - +
-
- - - {process.env.NODE_ENV === 'production' ? ( - - ) : ( - - )} - - +
+ {process.env.NODE_ENV === 'production' ? ( + + ) : ( + + )}
{addEntryButtons && } diff --git a/src/app/blog/_components/BadBlog/index.module.scss b/src/app/blog/_components/BadBlog/index.module.scss deleted file mode 100644 index 456402bb..00000000 --- a/src/app/blog/_components/BadBlog/index.module.scss +++ /dev/null @@ -1,121 +0,0 @@ -@use '@/styles/mixins'; -@use 'sass:math'; - -.bad_blog_wrapper { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--main-margin); - - @keyframes rainbow { - @for $i from 0 through 10 { - #{$i * 10}% { - background: hsl(36 * $i, 100%, 60%); - } - } - } - - // ebioishii_u - &[data-bad-blog='1'] > * { - color: white; - animation-name: atari-rainbow; - animation-duration: 1.2s; - animation-iteration-count: infinite; - animation-timing-function: linear; - user-select: none; - - @keyframes atari-rainbow { - @for $i from 0 through 10 { - #{$i * 10}% { - background: hsl(36 * $i, 100%, 60%); - font-size: #{0.5 + 2 * math.sin(($i + 1) / 10 * math.$pi)}em; - } - } - } - - @for $i from 0 through 1 { - * figure:nth-child(2n + #{$i}) * img { - animation-name: nobinobi-img#{$i}; - animation-duration: 0.8s; - animation-iteration-count: infinite; - } - } - - @keyframes nobinobi-img0 { - 0% { - transform: scale(0.8); - } - 50% { - transform: scale(1); - } - 0% { - transform: scale(0.8); - } - } - @keyframes nobinobi-img1 { - 0% { - transform: scale(1.2); - } - 50% { - transform: scale(1); - } - 0% { - transform: scale(1.2); - } - } - } - - // bad contrast - &[data-bad-blog='2'] { - --base-font-color: linen; - @include mixins.mq(dark) { - --base-font-color: midnightblue; - } - &, - h2, - h3 { - color: var(--base-font-color) !important; - } - } - - // invisible red - &[data-bad-blog='3'] { - @include mixins.mq(light) { - b, - strong { - opacity: 0.4; - color: linen; - } - } - } - - // too large images - &[data-bad-blog='4'] { - @for $i from 0 through 100 { - * figure:nth-child(100n + #{$i}) * img { - transform: scale(random(70) / 100 + 0.9) - rotate(random(360) + deg) - translate(random(100) / 100 * 30vw, random(100) / 100 * 30vh); - opacity: 0.9; - } - } - } - - // rotate - &[data-bad-blog='5'] { - @keyframes rotate-all { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - - article * * { - animation-name: rotate-all; - animation-timing-function: linear; - animation-duration: (random(10) + 3) + s; - animation-iteration-count: infinite; - } - } -} diff --git a/src/app/blog/_components/BadBlog/index.tsx b/src/app/blog/_components/BadBlog/index.tsx deleted file mode 100644 index bced0f04..00000000 --- a/src/app/blog/_components/BadBlog/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' -import { useContext, useState } from 'react' -import * as React from 'react' - -import { Button } from '@/components/atoms/Button' - -import styles from './index.module.scss' - -export const BadBlogStateContext = React.createContext({ - badBlog: 0, - setBadBlog: (() => {}) as any, - badButtonFlag: false, - setBadButtonFlag: (() => {}) as any, -}) - -export function BadBlogStateProvider(props: { children: React.ReactNode }) { - const [badBlog, setBadBlog] = useState(0) - const [badButtonFlag, setBadButtonFlag] = useState(false) - return ( - - {props.children} - - ) -} - -export function BadBlogButton() { - const badBlogs = [ - 'ebioishii_u', - 'コントラストがカス', - '赤が色褪せている', - '画像が散らかっている', - 'ぐるぐる', - ] - - const { badBlog, setBadBlog, badButtonFlag, setBadButtonFlag } = - useContext(BadBlogStateContext) - - const handleBadBlog = () => { - setBadButtonFlag(true) - if (badBlog) { - setBadBlog(0) - } else { - setBadBlog(Math.ceil(Math.random() * badBlogs.length)) - } - } - - return ( -
-

- -

- {badBlog > 0 && ( -

- よくないブログ No.{badBlog}: {badBlogs[badBlog - 1]} -

- )} -
- ) -} - -export function BadBlogBlock({ children }: { children: React.ReactNode }) { - const { badBlog } = useContext(BadBlogStateContext) - return ( -
- {children} -
- ) -} diff --git a/src/app/blog/_components/BlogH2.tsx b/src/app/blog/_components/BlogH2.tsx new file mode 100644 index 00000000..3bb8a6de --- /dev/null +++ b/src/app/blog/_components/BlogH2.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' + +import { faPaperclip } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { tv } from 'tailwind-variants' + +import { A } from '@/components/wrappers' +import { H2 } from '@/components/wrappers/H2' + +const styles = tv({ + slots: { + h2: [ + 'tw-relative tw-mt-10 tw-w-full', + 'tw-border-b-2 tw-border-solid tw-border-b-trpfrog-300', + ], + text: 'tw-peer tw-w-full', + anchor: [ + 'tw-absolute -tw-left-7 tw-top-0 tw-pr-2 tw-opacity-0 sp:tw-hidden', + 'peer-hover:tw-text-gray-300 peer-hover:tw-opacity-100', + 'hover:tw-text-body-color hover:tw-opacity-100', + ], + }, +})() + +export function BlogH2(props: React.ComponentPropsWithoutRef<'h2'>) { + const { className, children, ...rest } = props + return ( +

+ {children} + + + +

+ ) +} diff --git a/src/app/blog/_components/BlogImage/ImageWithModal.tsx b/src/app/blog/_components/BlogImage/ImageWithModal.tsx index 103f2764..66538c65 100644 --- a/src/app/blog/_components/BlogImage/ImageWithModal.tsx +++ b/src/app/blog/_components/BlogImage/ImageWithModal.tsx @@ -1,27 +1,6 @@ 'use client' -import { useState } from 'react' - -import { CldImageWrapper } from '@/components/utils/CldImageWrapper' - -import styles from '@blog/_components/BlogImage/index.module.scss' -import { getPureCloudinaryPath } from '@blog/_lib/cloudinaryUtils' - -function ImageSpoiler() { - const [spoilerState, setSpoilerState] = useState(true) - return spoilerState ? ( -
-
setSpoilerState(false)} - > - 画像を表示する -
-
- ) : ( - <> - ) -} +import { Image } from '@/components/atoms/Image' export function ImageWithModal(props: { src: string @@ -33,38 +12,13 @@ export function ImageWithModal(props: { let width = parseInt(searchParams.get('w') ?? '', 10) || 1000 let height = parseInt(searchParams.get('h') ?? '', 10) || 750 - const minWidth = 1000 - if (width < minWidth) { - height = Math.round(minWidth * (height / width)) - width = minWidth - } - - const maxHeight = 700 - if (height > maxHeight) { - width = Math.round(maxHeight * (width / height)) - height = maxHeight - } - - const srcPath = getPureCloudinaryPath(props.src).split('?')[0] - const blurPath = `https://res.cloudinary.com/trpfrog/image/upload/w_10${srcPath}` - return ( -
- - {props.spoiler && } -
+ {props.alt} ) } diff --git a/src/app/blog/_components/BlogImage/index.module.scss b/src/app/blog/_components/BlogImage/index.module.scss index 96aff279..24bfd3a8 100644 --- a/src/app/blog/_components/BlogImage/index.module.scss +++ b/src/app/blog/_components/BlogImage/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; .img_wrapper { display: flex; diff --git a/src/app/blog/_components/EntryButton/index.module.scss b/src/app/blog/_components/EntryButton/index.module.scss index 9a8b4695..1fb1bb0b 100644 --- a/src/app/blog/_components/EntryButton/index.module.scss +++ b/src/app/blog/_components/EntryButton/index.module.scss @@ -1,8 +1,8 @@ -@use '@/styles/mixins'; -@use '@/styles/common/linkbutton'; +@use '../../../../styles/mixins'; +@use '../../../../components/atoms/MagicButton/index.module.scss' as button; .entry_button { - @extend .linkButton; + @extend .button; text-align: center; display: inline-block; diff --git a/src/app/blog/_components/LiteArticleCard/index.module.scss b/src/app/blog/_components/LiteArticleCard/index.module.scss index e542cb5b..eb792819 100644 --- a/src/app/blog/_components/LiteArticleCard/index.module.scss +++ b/src/app/blog/_components/LiteArticleCard/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; .wrapper { border-radius: 20px !important; diff --git a/src/app/blog/_components/PageNavigation.tsx b/src/app/blog/_components/PageNavigation.tsx index 5ced2ac5..b9975dd4 100644 --- a/src/app/blog/_components/PageNavigation.tsx +++ b/src/app/blog/_components/PageNavigation.tsx @@ -1,7 +1,7 @@ -import { Button } from '@/components/atoms/Button' - import { BlogPost } from '@blog/_lib/blogPost' +import { MagicButton } from 'src/components/atoms/MagicButton' + type Props = { entry: BlogPost doNotShowOnFirst?: boolean @@ -28,13 +28,13 @@ export const PageTransferButton = (props: PageTransferProps) => { return entry.isAll ? ( <> ) : ( - + ) } diff --git a/src/app/blog/_components/PostAttributes/index.module.scss b/src/app/blog/_components/PostAttributes/index.module.scss index 58098924..5b6f5ec2 100644 --- a/src/app/blog/_components/PostAttributes/index.module.scss +++ b/src/app/blog/_components/PostAttributes/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; #post_attributes { display: flex; diff --git a/src/app/blog/_components/RelatedPosts.tsx b/src/app/blog/_components/RelatedPosts.tsx index 5cc5af48..369bcf47 100644 --- a/src/app/blog/_components/RelatedPosts.tsx +++ b/src/app/blog/_components/RelatedPosts.tsx @@ -1,12 +1,12 @@ 'use client' import { faStar } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Button } from '@/components/atoms/Button' +import { OnBodyHeading } from '@/components/atoms/OnBodyHeading' import { ArticleGrid } from '@blog/_components/ArticleGrid' import { BlogPost } from '@blog/_lib/blogPost' -import styles from '@blog/_styles/blog.module.scss' + +import { MagicButton } from 'src/components/atoms/MagicButton' import { ArticleCard } from './ArticleCard' @@ -22,13 +22,10 @@ export const RelatedPosts = ({ } else { return ( <> -
- タグ「{tag}」の新着記事{' '} - -
+ タグ「{tag}」の新着記事 {relatedPosts.slice(0, 6).map((e, idx) => ( -
2 ? 'only-on-pc' : ''}> +
2 ? 'sp:tw-hidden' : ''}>
))} @@ -36,19 +33,19 @@ export const RelatedPosts = ({ {/* PC */} {relatedPosts.length > 6 && ( -
- +
)} {/* SMARTPHONES */} {relatedPosts.length > 3 && ( -
- +
)} diff --git a/src/app/blog/_components/Tag/index.module.scss b/src/app/blog/_components/Tag/index.module.scss index 42e34d6f..35fffd78 100644 --- a/src/app/blog/_components/Tag/index.module.scss +++ b/src/app/blog/_components/Tag/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; .block { display: inline-flex; diff --git a/src/app/blog/_components/TogglePageViewLink.tsx b/src/app/blog/_components/TogglePageViewLink.tsx index 4d732d49..b9aa8085 100644 --- a/src/app/blog/_components/TogglePageViewLink.tsx +++ b/src/app/blog/_components/TogglePageViewLink.tsx @@ -4,6 +4,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core' import { faFileLines, faToiletPaper } from '@fortawesome/free-solid-svg-icons' import { usePathname } from 'next/navigation' +import { A } from '@/components/wrappers' + import { BlogPost } from '@blog/_lib/blogPost' import { EntryButton } from './EntryButton' @@ -34,8 +36,8 @@ export function TogglePageViewLink({ post }: { post: BlogPost }) { } return ( - + - + ) } diff --git a/src/app/blog/_components/UDFontBlock.tsx b/src/app/blog/_components/UDFontBlock.tsx deleted file mode 100644 index c9453f8b..00000000 --- a/src/app/blog/_components/UDFontBlock.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client' -import { useContext, useEffect, useState } from 'react' -import * as React from 'react' - -import { faFont, faUniversalAccess } from '@fortawesome/free-solid-svg-icons' -import { parseCookies, setCookie } from 'nookies' - -import styles from '@blog/_styles/blog.module.scss' - -import { EntryButton } from './EntryButton' - -export const UDFontStateContext = React.createContext({ - useUDFont: false, - setUseUDFont: (() => {}) as any, -}) - -export function UDFontStateProvider(props: { children: React.ReactNode }) { - const COOKIE_NAME_UD = 'useUDFonts' - const cookie = parseCookies() - const [useUDFont, setUseUDFont] = useState(false) - useEffect(() => { - setUseUDFont(cookie[COOKIE_NAME_UD] === 'true') - }, [cookie]) - - return ( - - {props.children} - - ) -} - -export function UDFontButton() { - const COOKIE_NAME_UD = 'useUDFonts' - const { useUDFont, setUseUDFont } = useContext(UDFontStateContext) - - const handleUDFontButton = () => { - if (useUDFont) { - setCookie(null, COOKIE_NAME_UD, 'false', { - maxAge: 1, - path: '/', - }) - } else { - setCookie(null, COOKIE_NAME_UD, 'true', { - maxAge: 30 * 24 * 60 * 60, - path: '/', - }) - } - setUseUDFont(!useUDFont) - } - - return ( - - {useUDFont ? ( - - ) : ( - - )} - - ) -} - -export function UDFontBlock({ children }: { children: React.ReactNode }) { - const { useUDFont } = useContext(UDFontStateContext) - - return
{children}
-} diff --git a/src/app/blog/_components/article-parts/ProfileCards/index.module.scss b/src/app/blog/_components/article-parts/ProfileCards/index.module.scss index 4357f74b..8a22fa10 100644 --- a/src/app/blog/_components/article-parts/ProfileCards/index.module.scss +++ b/src/app/blog/_components/article-parts/ProfileCards/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../../styles/mixins'; .profile_card_grid { display: grid; diff --git a/src/app/blog/_components/article-parts/ProfileCards/index.tsx b/src/app/blog/_components/article-parts/ProfileCards/index.tsx index 19157574..bf27559b 100644 --- a/src/app/blog/_components/article-parts/ProfileCards/index.tsx +++ b/src/app/blog/_components/article-parts/ProfileCards/index.tsx @@ -1,7 +1,8 @@ import dayjs from 'dayjs' import { z } from 'zod' -import { Button } from '@/components/atoms/Button' +import { InlineLink } from '@/components/atoms/InlineLink' +import { Li, UnorderedList } from '@/components/wrappers' import { createURL } from '@/lib/url' @@ -10,6 +11,8 @@ import { ArticleParts } from '@blog/_components/ArticleParts' import { parseObjectList } from '@blog/_lib/rawTextParser' import { parseInlineMarkdown } from '@blog/_renderer/BlogMarkdown' +import { MagicButton } from 'src/components/atoms/MagicButton' + import styles from './index.module.scss' const ProfileDataSchema = z.object({ @@ -32,9 +35,9 @@ const CardFormat = ({
{personalData.club}
@@ -56,25 +59,25 @@ const ListFormat = ({ }: { personalDataList: ProfileData[] }) => ( -
    + {personalDataList.map((personalData: any) => ( <> -
  • +
  • {personalData.name} {personalData.name === 'つまみ' ? ' (筆者)' : 'さん'} -
  • - + + +
  • {parseInlineMarkdown(personalData.description)}
  • +
    ))} -
+ ) export const profileCardParts = { @@ -107,9 +110,9 @@ export const profileCardParts = { secondaryButtonText={'カード表示に切り替え'} /> {twitterSearchLink !== '' && ( - + )} ) diff --git a/src/app/blog/_components/article-parts/Twitter/index.tsx b/src/app/blog/_components/article-parts/Twitter/index.tsx index be6d5725..df0174f1 100644 --- a/src/app/blog/_components/article-parts/Twitter/index.tsx +++ b/src/app/blog/_components/article-parts/Twitter/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react' +import { InlineLink } from '@/components/atoms/InlineLink' import { Tweet } from '@/components/utils/TweetWrapper' import { ArticleParts } from '@blog/_components/ArticleParts' @@ -17,7 +18,10 @@ export const twitterParts = { Tweet not found
- id = {id} + id ={' '} + + {id} +

} @@ -32,10 +36,10 @@ export const twitterParts = { width: 'min(550px, 100%)', }} > -
+
{tweet}
-
+
{tweet}
diff --git a/src/app/blog/_components/article-parts/TwitterArchive/index.module.scss b/src/app/blog/_components/article-parts/TwitterArchive/index.module.scss index d46bd2e7..7c031c20 100644 --- a/src/app/blog/_components/article-parts/TwitterArchive/index.module.scss +++ b/src/app/blog/_components/article-parts/TwitterArchive/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../../styles/mixins'; .wrapper { width: 100%; diff --git a/src/app/blog/_components/article-parts/WalkingResultBox/index.module.scss b/src/app/blog/_components/article-parts/WalkingResultBox/index.module.scss index 348b6566..9b18e0d2 100644 --- a/src/app/blog/_components/article-parts/WalkingResultBox/index.module.scss +++ b/src/app/blog/_components/article-parts/WalkingResultBox/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../../styles/mixins'; .result_box_title { } diff --git a/src/app/blog/_components/article-parts/YouTube/InnerYouTube.tsx b/src/app/blog/_components/article-parts/YouTube/InnerYouTube.tsx index 3232eda5..13a3324d 100644 --- a/src/app/blog/_components/article-parts/YouTube/InnerYouTube.tsx +++ b/src/app/blog/_components/article-parts/YouTube/InnerYouTube.tsx @@ -3,7 +3,17 @@ import { memo } from 'react' import ReactPlayer from 'react-player/youtube' -import YouTube from 'react-youtube' +import { tv } from 'tailwind-variants' + +import { YouTube } from '@/components/organisms/YouTube' + +const youtubeStyles = tv({ + slots: { + wrapper: 'tw-text-center', + base: 'tw-relative tw-h-0 tw-w-full tw-overflow-hidden tw-rounded-lg tw-pb-[50%]', + iframe: 'tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-full', + }, +})() export const InnerYouTube = memo(function InnerYouTube({ content, @@ -13,15 +23,7 @@ export const InnerYouTube = memo(function InnerYouTube({ const lines = content.split('\n') const id = lines[0].trim() const title = lines[1]?.trim() - return ( -
- -
- ) + return }) export const InnerAutoYouTube = memo(function InnerAutoYouTube({ @@ -33,13 +35,7 @@ export const InnerAutoYouTube = memo(function InnerAutoYouTube({ const id = lines[0].trim() const title = lines[1]?.trim() return ( -
+
& { - components: IsomorphicMarkdownComponent + components: MDXComponents } export type ArticleRendererProps = @@ -28,7 +27,7 @@ export const ArticleRenderer = memo(function ArticleRenderer( if ('markdownOptions' in props) { options = props.markdownOptions } else { - options = getMarkdownOptions(props.entry) + options = getMarkdownOptions({ entry: props.entry }) } return diff --git a/src/app/blog/_renderer/BlogMarkdown.tsx b/src/app/blog/_renderer/BlogMarkdown.tsx index 51a188bb..1f2564a3 100644 --- a/src/app/blog/_renderer/BlogMarkdown.tsx +++ b/src/app/blog/_renderer/BlogMarkdown.tsx @@ -12,7 +12,7 @@ import { ArticleRenderer } from './ArticleRenderer' import { getMarkdownOptions } from './rendererProperties' export const parseInlineMarkdown = (markdown: string) => { - const options = getMarkdownOptions(undefined, true) + const options = getMarkdownOptions({ inline: true }) return } diff --git a/src/app/blog/_renderer/DevBlogMarkdown/ImageDragAndDrop.module.scss b/src/app/blog/_renderer/DevBlogMarkdown/ImageDragAndDrop.module.scss index 319d4afb..3f218d3a 100644 --- a/src/app/blog/_renderer/DevBlogMarkdown/ImageDragAndDrop.module.scss +++ b/src/app/blog/_renderer/DevBlogMarkdown/ImageDragAndDrop.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../../styles/mixins'; .wrapper { display: block; diff --git a/src/app/blog/_renderer/rendererProperties.tsx b/src/app/blog/_renderer/rendererProperties.tsx index 1dadb527..fe6d979b 100644 --- a/src/app/blog/_renderer/rendererProperties.tsx +++ b/src/app/blog/_renderer/rendererProperties.tsx @@ -1,7 +1,5 @@ import * as React from 'react' -import { faPaperclip } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MDXComponents } from 'mdx/types' import { SerializeOptions } from 'next-mdx-remote/dist/types' import 'katex/dist/katex.min.css' @@ -13,12 +11,14 @@ import remarkMath from 'remark-math' import remarkToc from 'remark-toc' import remarkUnwrapImages from 'remark-unwrap-images' -import { OpenInNewTab } from '@/components/atoms/OpenInNewTab' +import { InlineLink } from '@/components/atoms/InlineLink' import { CodeBlock, CodeBlockProps } from '@/components/molecules/CodeBlock' import { parseDataLine } from '@/components/molecules/CodeBlock/parseDataLine' +import * as Wrapper from '@/components/wrappers' -import { IsomorphicMarkdownComponent } from '@/lib/types' +import { twMerge } from '@/lib/tailwind/merge' +import { BlogH2 } from '@blog/_components/BlogH2' import { BlogImage } from '@blog/_components/BlogImage' import { isValidExtraCodeBlockComponentName, @@ -118,10 +118,14 @@ function styledTag(tag: React.ElementType, className: string) { } } -export function getMarkdownOptions(entry?: BlogPost, isInline?: boolean) { - const components: IsomorphicMarkdownComponent = { +export function getMarkdownOptions(options?: { + entry?: BlogPost + inline?: boolean + openInNewTab?: 'always' | 'external' | 'never' +}) { + const components: MDXComponents = { pre: ({ children }: any) =>
{children}
, // disable pre tag - code: formatCodeComponentFactory(entry), + code: formatCodeComponentFactory(options?.entry), img: (props: any) => { return ( @@ -137,24 +141,27 @@ export function getMarkdownOptions(entry?: BlogPost, isInline?: boolean) { }, p: (props: React.ComponentProps<'p'>) => { - return React.createElement(isInline ? 'span' : 'p', props) + return React.createElement(options?.inline ? 'span' : 'p', props) }, - h2: (props: any) => ( -

- - - - {props.children} -

- ), + h2: (props: any) => , + h3: (props: any) => , + h4: (props: any) => , + h5: (props: any) => , + + ul: (props: any) => , + ol: (props: any) => , + li: (props: any) => , + a: (props: any) => ( - {props.children} + + {props.children} + ), + kbd: (props: any) => , blockquote: styledTag('blockquote', styles.blockquote), details: styledTag('details', styles.details), summary: styledTag('summary', styles.summary), - h3: styledTag('h3', styles.h3), video: (props: any) => ( ), + + hr: (props: any) => { + const { className = '', ...rest } = props + return + }, + + table: (props: any) => { + let { className, ...rest } = props + className = twMerge('tw-mx-auto', className) + return + }, } return { diff --git a/src/app/blog/_styles/articles.scss b/src/app/blog/_styles/articles.scss index b971f329..db90d0e3 100644 --- a/src/app/blog/_styles/articles.scss +++ b/src/app/blog/_styles/articles.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../styles/mixins'; .hrule_block { font-family: var(--font-m-plus-rounded-1c); @@ -21,38 +21,6 @@ } } -.anchor { - width: 100%; - border-bottom: 1px gray solid; - padding-left: 1px; - - @include mixins.mq(pc) { - position: relative; - a { - position: absolute; - left: -1.2em; - top: 0.2em; - opacity: 0; - margin-right: 0.3em; - } - &:hover { - a { - opacity: 1; - color: lightgray; - - &:hover { - color: #90e200; - } - } - } - } - @include mixins.mq(sp) { - a { - display: none; - } - } -} - #hero_article { @include mixins.mq(sp) { column-gap: 8px; diff --git a/src/app/blog/_styles/blog.module.scss b/src/app/blog/_styles/blog.module.scss index 729f3847..37bb8782 100644 --- a/src/app/blog/_styles/blog.module.scss +++ b/src/app/blog/_styles/blog.module.scss @@ -1,6 +1,6 @@ @use 'articles'; -@use '@/styles/mixins'; +@use '../../../styles/mixins'; .article_block { padding: 17px; @@ -82,21 +82,6 @@ } } -.with_ud_font { - font-family: var(--font-roboto), var(--font-biz-udp-gothic), var(--main-font) !important; - letter-spacing: 1px; - - * { - --main-font: var(--font-roboto), var(--font-biz-udp-gothic), - var(--main-font); - } - - strong, - b { - -webkit-text-stroke: 0.5px var(--base-font-color); - } -} - .main_content > aside { display: none; } @@ -130,7 +115,7 @@ } .post code { - font-family: var(--font-noto-sans-mono), var(--font-noto-sans-jp), monospace !important; + font-family: var(--font-noto-sans-mono), monospace !important; } .inline_code_block { @@ -142,7 +127,7 @@ background-color: #313131 !important; color: white; } - font-family: var(--font-noto-sans-mono), var(--font-noto-sans-jp), monospace; + font-family: var(--font-noto-sans-mono), monospace; padding: 0 4px; font-size: 0.9em; border-radius: 5px; @@ -183,7 +168,6 @@ } &::after { - font-family: var(--font-noto-sans-jp); transition: 100ms; font-weight: bold; color: var(--base-font-color); @@ -215,10 +199,6 @@ } } -.h3 { - margin-top: 2em; -} - .details { @extend .pretty_details; } diff --git a/src/app/blog/layout.tsx b/src/app/blog/layout.tsx index 1974e53e..21b81af8 100644 --- a/src/app/blog/layout.tsx +++ b/src/app/blog/layout.tsx @@ -2,9 +2,6 @@ import * as React from 'react' import { Metadata } from 'next' -import { BadBlogStateProvider } from '@blog/_components/BadBlog' -import { UDFontStateProvider } from '@blog/_components/UDFontBlock' - export const metadata: Metadata = { title: { absolute: 'つまみログ', @@ -17,9 +14,5 @@ type Props = { } export default function BlogLayout({ children }: Props) { - return ( - - {children} - - ) + return children } diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index e696b527..11a3fe30 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -3,10 +3,10 @@ import { Fragment } from 'react' import { Metadata } from 'next' import { faStar } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import dayjs from 'dayjs' import { MainWrapper } from '@/components/atoms/MainWrapper' +import { OnBodyHeading } from '@/components/atoms/OnBodyHeading' import { Title } from '@/components/organisms/Title' import { getTypedEntries } from '@/lib/utils' @@ -45,13 +45,10 @@ export default async function Index() { return ( <> - + - <div className={styles.hrule_block}> - <FontAwesomeIcon icon={faStar} /> FEATURED ARTICLE{' '} - <FontAwesomeIcon icon={faStar} /> - </div> + <OnBodyHeading icon={faStar}>FEATURED ARTICLE</OnBodyHeading> <div id={styles.hero_article}> <ArticleCard entry={latestFeaturedArticle} hero={true} /> @@ -61,10 +58,7 @@ export default async function Index() { .reverse() .map(([year, articles]) => ( <Fragment key={year as string}> - <div className={styles.hrule_block}> - <FontAwesomeIcon icon={faStar} /> {year as string} 年{' '} - <FontAwesomeIcon icon={faStar} /> - </div> + <OnBodyHeading icon={faStar}>{year as string} 年</OnBodyHeading> <div style={{ display: 'flex', flexDirection: 'column', gap: 15 }} diff --git a/src/app/blog/preview/[...slug]/page.tsx b/src/app/blog/preview/[...slug]/page.tsx index 6410d05b..d9672f5b 100644 --- a/src/app/blog/preview/[...slug]/page.tsx +++ b/src/app/blog/preview/[...slug]/page.tsx @@ -54,7 +54,7 @@ export default async function Index(props: Props) { const { minutes: readMin, seconds: readSec } = formatReadTime(post.readTime) return ( - <MainWrapper> + <MainWrapper gridLayout> <ArticleHeader post={post} addEntryButtons={false} /> <Block> {!post.isError && ( diff --git a/src/app/blog/tags/[tag]/page.tsx b/src/app/blog/tags/[tag]/page.tsx index 740c2269..a984eede 100644 --- a/src/app/blog/tags/[tag]/page.tsx +++ b/src/app/blog/tags/[tag]/page.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/components/atoms/Button' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Title } from '@/components/organisms/Title' @@ -9,6 +8,8 @@ import { retrieveExistingAllTags, } from '@blog/_lib/load' +import { MagicButton } from 'src/components/atoms/MagicButton' + export async function generateStaticParams() { const tags = await retrieveExistingAllTags() return tags.map(tag => ({ @@ -37,11 +38,11 @@ export default async function Index({ params }: Props) { return ( <> - <MainWrapper> + <MainWrapper gridLayout> <Title> <h1>タグ「{tag}」の記事一覧</h1> <p> - <Button href={'/blog'}>記事一覧に戻る</Button> + <MagicButton href={'/blog'}>記事一覧に戻る</MagicButton> </p> diff --git a/src/app/certification/page.tsx b/src/app/certification/page.tsx index 3dee5be3..4d96924c 100644 --- a/src/app/certification/page.tsx +++ b/src/app/certification/page.tsx @@ -38,7 +38,7 @@ export default async function Index() { ) return ( - + <Block> <div id={styles.cert_grid}> diff --git a/src/app/download/contents/icon-wallpaper.md b/src/app/download/contents/icon-wallpaper.md index de76f269..2e03e998 100644 --- a/src/app/download/contents/icon-wallpaper.md +++ b/src/app/download/contents/icon-wallpaper.md @@ -2,7 +2,7 @@ title: "壁紙: アイコン集合" description: Twitterのヘッダー用に作ったものを壁紙に作り直しました。 image: - src: download/wallpapers/icons/thumbnail + src: /download/wallpapers/icons/thumbnail alt: アイコン集合の壁紙 width: 1000 height: 625 @@ -15,7 +15,7 @@ links: href: https://res.cloudinary.com/trpfrog/download/wallpapers/icons/smartphone.png - text: 縦長 (1:2) href: https://res.cloudinary.com/trpfrog/download/wallpapers/icons/iphonex.png -date: 2019/6/24 +date: 2019-06-24 --- PC用は2560×1600pxです。 diff --git a/src/app/download/contents/oni-watchface.md b/src/app/download/contents/oni-watchface.md index 01e8c048..16032018 100644 --- a/src/app/download/contents/oni-watchface.md +++ b/src/app/download/contents/oni-watchface.md @@ -2,14 +2,14 @@ title: 鬼のウォッチフェイス description: Apple Watch の文字盤です。誰得? image: - src: download/watchfaces/oni/thumbnail + src: /download/watchfaces/oni/thumbnail alt: 鬼のウォッチフェイスの画像 width: 512 height: 384 links: - text: ダウンロード href: https://res.cloudinary.com/trpfrog/raw/upload/v1641138633/download/watchfaces/oni/oni.watchface -date: 2022/1/3 +date: 2022-01-03 --- iPhoneからDLしてください diff --git a/src/app/download/contents/rainy-wallpaper.md b/src/app/download/contents/rainy-wallpaper.md index c9d7e5a9..e9138526 100644 --- a/src/app/download/contents/rainy-wallpaper.md +++ b/src/app/download/contents/rainy-wallpaper.md @@ -2,7 +2,7 @@ title: "壁紙: 雨" description: 天気の子の陽菜ちゃんになりたくて作りました。天気の子は観た方が良いです。 image: - src: download/wallpapers/rainy/thumbnail + src: /download/wallpapers/rainy/thumbnail alt: 雨の壁紙 width: 1000 height: 625 @@ -23,7 +23,7 @@ links: href: https://res.cloudinary.com/trpfrog/download/wallpapers/rainy/iphonex.png - text: 縦長 (1:2, 背景のみ) href: https://res.cloudinary.com/trpfrog/download/wallpapers/rainy/iphonex_bkg.png -date: 2019/6/23 +date: 2019-06-23 --- PC用は右にかけて暗くなっていくグラデーションあり版があります。 diff --git a/src/app/download/page.tsx b/src/app/download/page.tsx index 70b6eeb3..1cffb0e1 100644 --- a/src/app/download/page.tsx +++ b/src/app/download/page.tsx @@ -2,44 +2,34 @@ import path from 'path' import { Metadata } from 'next' -import Image from 'next/legacy/image' -import ReactMarkdown from 'react-markdown' +import { MDXRemote } from 'next-mdx-remote/rsc' -import { Button } from '@/components/atoms/Button' +import { WorksFrontmatterSchema } from '@/app/download/schema' + +import { Image } from '@/components/atoms/Image' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' import { readMarkdowns } from '@/lib/mdLoader' +import { getMarkdownOptions } from '@blog/_renderer/rendererProperties' + +import { MagicButton } from 'src/components/atoms/MagicButton' + export const metadata = { title: 'DLコンテンツ', description: '壁紙などダウンロードできるコンテンツの提供ページです。', } satisfies Metadata -type Frontmatter = { - title: string - description: string - image: { - src: string - alt?: string - width: number - height: number - } - links: { - href: string - text: string - }[] - date: `${number}/${number}/${number}` -} - export default async function Index() { - const contents = await readMarkdowns<Frontmatter>( + const contents = await readMarkdowns( path.join(process.cwd(), 'src', 'app', 'download', 'contents'), + WorksFrontmatterSchema, ) return ( - <MainWrapper> + <MainWrapper gridLayout> <Title title={metadata.title} description={metadata.description} /> {contents.map(({ metadata, content }) => { return ( @@ -51,20 +41,18 @@ export default async function Index() { src={metadata.image.src} width={metadata.image.width} height={metadata.image.height} - className={'rich_image'} - layout={'intrinsic'} alt={metadata.image.alt} /> </div> )} - <ReactMarkdown>{content}</ReactMarkdown> + <MDXRemote source={content} {...getMarkdownOptions()} /> <div style={{ display: 'flex', flexFlow: 'row wrap', gap: '8px 6px' }} > {metadata.links.map(({ href, text }) => ( - <Button key={href} href={href}> + <MagicButton key={href} href={href}> {text} - </Button> + </MagicButton> ))} </div> </Block> diff --git a/src/app/download/schema.ts b/src/app/download/schema.ts new file mode 100644 index 00000000..53fa4401 --- /dev/null +++ b/src/app/download/schema.ts @@ -0,0 +1,21 @@ +import { z } from 'zod' + +export const WorksFrontmatterSchema = z.object({ + title: z.string(), + description: z.string(), + image: z.object({ + src: z.string(), + alt: z.string().optional(), + width: z.number(), + height: z.number(), + }), + links: z.array( + z.object({ + href: z.string(), + text: z.string(), + }), + ), + date: z.coerce.date(), +}) + +export type WorksFrontmatter = z.infer<typeof WorksFrontmatterSchema> diff --git a/src/app/environment/GadgetIntro.tsx b/src/app/environment/GadgetIntro.tsx index 63c80250..21a6facf 100644 --- a/src/app/environment/GadgetIntro.tsx +++ b/src/app/environment/GadgetIntro.tsx @@ -2,6 +2,8 @@ import * as React from 'react' import Image from 'next/legacy/image' +import { H4 } from '@/components/wrappers' + import styles from './style.module.scss' type Props = { @@ -32,7 +34,7 @@ export const GadgetIntro: React.FunctionComponent<Props> = ({ } return ( <> - <h4 className={styles.name}>{name}</h4> + <H4 className={styles.name}>{name}</H4> {imageHtml} {children} </> diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx index d49e5a62..848eab2e 100644 --- a/src/app/environment/page.tsx +++ b/src/app/environment/page.tsx @@ -10,6 +10,7 @@ import yaml from 'js-yaml' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' +import { H3, Li, UnorderedList } from '@/components/wrappers' import { GadgetIntro } from './GadgetIntro' @@ -45,23 +46,23 @@ function Itemize(props: { return <p>{props.children}</p> } return ( - <ul> + <UnorderedList> {props.children.map((child, index) => { if (typeof child === 'string') { - return <li key={index}>{child}</li> + return <Li key={index}>{child}</Li> } return ( <Fragment key={index}> {Object.entries(child).map(([key, value]) => ( <Fragment key={key}> - <li>{key}</li> + <Li>{key}</Li> <Itemize>{value}</Itemize> </Fragment> ))} </Fragment> ) })} - </ul> + </UnorderedList> ) } @@ -84,7 +85,7 @@ export default async function Index() { const items = yaml.load(yamlText) as Items return ( - <MainWrapper> + <MainWrapper gridLayout> <Title title={metadata.title} description={metadata.description}> <p>Last updated: 2021/12/11</p> <GadgetIntro name="" imagePath="desk" /> @@ -94,7 +95,7 @@ export default async function Index() { <Block title={genre.categoryName} h2icon="think" key={key}> {genre.items.map(item => ( <div key={item.name}> - <h3>{item.name}</h3> + <H3>{item.name}</H3> {item.items.map(gadget => ( <GadgetIntro name={gadget.productName} diff --git a/src/app/environment/style.module.scss b/src/app/environment/style.module.scss index 5d1a7635..abd66f5b 100644 --- a/src/app/environment/style.module.scss +++ b/src/app/environment/style.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../styles/mixins'; .name { font-weight: bold; diff --git a/src/app/fuzzy/SearchBox.tsx b/src/app/fuzzy/SearchBox.tsx index 52c8edce..6c8e3081 100644 --- a/src/app/fuzzy/SearchBox.tsx +++ b/src/app/fuzzy/SearchBox.tsx @@ -1,21 +1,31 @@ 'use client' +import { tv } from 'tailwind-variants' + +import { Input } from '@/components/wrappers' + +const styles = tv({ + slots: { + form: 'tw-flex tw-items-center tw-justify-center tw-gap-1', + }, +})() + export function SearchBox() { return ( - <div> - https://trpfrog.net/fuzzy/{' '} - <input type="text" name="url" placeholder="fuzzy-string" /> - <input - type="submit" - value="Go" - onClick={() => { - // @ts-ignore - const url = document.querySelector('input[name="url"]').value - if (url) { - window.location.href = `https://trpfrog.net/fuzzy/${url}` - } - }} - /> - </div> + <form + className={styles.form()} + onSubmit={e => { + e.preventDefault() + const form = new FormData(e.currentTarget) + const url = form.get('url') + if (typeof url === 'string' && url) { + window.location.href = `https://trpfrog.net/fuzzy/${url}` + } + }} + > + <div>https://trpfrog.net/fuzzy/</div> + <Input type="text" name="url" placeholder="fuzzy-string" pattern=".+" /> + <Input type="submit" value="Go" /> + </form> ) } diff --git a/src/app/fuzzy/[input]/route.ts b/src/app/fuzzy/[input]/route.ts index 924f9e33..48b1c3fe 100644 --- a/src/app/fuzzy/[input]/route.ts +++ b/src/app/fuzzy/[input]/route.ts @@ -1,5 +1,5 @@ -import { ChatOpenAI } from 'langchain/chat_models/openai' -import { HumanMessage } from 'langchain/schema' +import { BaseMessageChunk, HumanMessage } from '@langchain/core/messages' +import { ChatOpenAI } from '@langchain/openai' import { NextRequest, NextResponse } from 'next/server' import { createRateLimit } from '@/lib/rateLimit' @@ -38,6 +38,22 @@ type GETProps = { } } +function extractTextFromBaseMessageChunk(chunk: BaseMessageChunk) { + const content = chunk.content + if (typeof content === 'string') { + return content + } else { + const content = chunk.content[0] + if (typeof content === 'string') { + return content + } + if (content.type === 'text') { + return content.text + } + } + return '' +} + export async function GET(req: NextRequest, props: GETProps) { const res = NextResponse.next() @@ -69,8 +85,8 @@ export async function GET(req: NextRequest, props: GETProps) { try { await limiter.check(res, 5, req.ip ?? 'ip_not_found') - const chatResponse = await chat.call([new HumanMessage(prompt)]) - const output = chatResponse.content.trim() + const chatResponse = await chat.invoke([new HumanMessage(prompt)]) + const output = extractTextFromBaseMessageChunk(chatResponse) const url = blogPaths.includes(output) ? '/blog/' + output : '/' + output return NextResponse.redirect(new URL(url, req.url)) } catch { diff --git a/src/app/fuzzy/layout.tsx b/src/app/fuzzy/layout.tsx index d8d4f509..870e286e 100644 --- a/src/app/fuzzy/layout.tsx +++ b/src/app/fuzzy/layout.tsx @@ -14,5 +14,5 @@ type Props = { } export default function RootLayout({ children }: Props) { - return <MainWrapper>{children}</MainWrapper> + return <MainWrapper gridLayout>{children}</MainWrapper> } diff --git a/src/app/fuzzy/page.tsx b/src/app/fuzzy/page.tsx index 4a7ef60c..1f561c0f 100644 --- a/src/app/fuzzy/page.tsx +++ b/src/app/fuzzy/page.tsx @@ -1,4 +1,6 @@ +import { InlineLink } from '@/components/atoms/InlineLink' import { Block } from '@/components/molecules/Block' +import { Li, UnorderedList } from '@/components/wrappers' import { SearchBox } from './SearchBox' import styles from './style.module.scss' @@ -18,26 +20,26 @@ export default function Fuzzy() { </Block> <Block title="入力例" h2icon="trpfrog"> - <ul> - <li> + <UnorderedList> + <Li> enoshima {'=> '} - <a href={'https://trpfrog.net/blog/enoshima-walk'}> + <InlineLink href={'https://trpfrog.net/blog/enoshima-walk'}> https://trpfrog.net/blog/enoshima-walk - </a> - </li> - <li> + </InlineLink> + </Li> + <Li> balllloooon {'=> '} - <a href={'https://trpfrog.net/balloon'}> + <InlineLink href={'https://trpfrog.net/balloon'}> https://trpfrog.net/balloon - </a> - </li> - <li> + </InlineLink> + </Li> + <Li> 山登り {'=> '} - <a href={'https://trpfrog.net/blog/takao-full-search'}> + <InlineLink href={'https://trpfrog.net/blog/takao-full-search'}> https://trpfrog.net/blog/takao-full-search - </a> - </li> - </ul> + </InlineLink> + </Li> + </UnorderedList> </Block> <Block title="仕組み" h2icon="robot"> diff --git a/src/app/fuzzy/style.module.scss b/src/app/fuzzy/style.module.scss index 7af79961..46cdd93b 100644 --- a/src/app/fuzzy/style.module.scss +++ b/src/app/fuzzy/style.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../styles/mixins'; .title { font-size: 4rem; diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 00000000..0093fec4 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,40 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + *:focus { + outline-color: #90e200; + } + + :root { + --color-body: 192 225 121; + --color-text: 21 21 21; + --color-window: 255 255 255; + --color-header: 102 169 40; + } + + @media screen and (prefers-color-scheme: dark) { + :root { + --color-body: 61 77 34; + --color-text: 229 229 229; + --color-window: 32 32 32; + --color-header: 79 131 31; + } + } + + p { + @apply tw-my-4; + } + + article { + @apply tw-leading-relaxed tw-tracking-wide; + } +} + +@layer utilities { + .break-anywhere { + overflow-wrap: anywhere; + word-break: keep-all; + } +} diff --git a/src/app/icon-maker/IconMakerApp.tsx b/src/app/icon-maker/IconMakerApp.tsx index 044cb4d0..17c13a52 100644 --- a/src/app/icon-maker/IconMakerApp.tsx +++ b/src/app/icon-maker/IconMakerApp.tsx @@ -1,81 +1,38 @@ 'use client' -import Image from 'next/legacy/image' +import { useRef } from 'react' + +import { + useIconMakerController, + useIconMakerRef, +} from '@/app/icon-maker/iconMakerHooks' -import { Button } from '@/components/atoms/Button' import { Block } from '@/components/molecules/Block' +import { Input } from '@/components/wrappers' -import { IconCanvas } from '@/lib/iconMaker' import { createURL } from '@/lib/url' +import { MagicButton } from 'src/components/atoms/MagicButton' + import styles from './style.module.scss' export function IconMakerApp() { - const state = new IconCanvas('canvas-result') + const canvasRef = useRef<HTMLCanvasElement>(null) + const state = useIconMakerRef(canvasRef) + const controlButtons = useIconMakerController(state) + const tweetLink = createURL('/intent/tweet', 'https://twitter.com', { text: '#つまみアイコンメーカー でアイコンを作成しました!', url: 'https://trpfrog.net/iconmaker/', }) - const controlButtons = [ - { - className: styles.plus_btn, - onClick: () => state.scaleImage(1.05), - text: '+', - }, - { - className: styles.minus_btn, - onClick: () => state.scaleImage(1 / 1.05), - text: '-', - }, - { - className: styles.left_btn, - onClick: () => state.moveImage(-5, 0), - text: '←', - }, - { - className: styles.down_btn, - onClick: () => state.moveImage(0, 5), - text: '↓', - }, - { - className: styles.up_btn, - onClick: () => state.moveImage(0, -5), - text: '↑', - }, - { - className: styles.right_btn, - onClick: () => state.moveImage(0, 5), - text: '→', - }, - { - className: styles.rotate_left_btn, - onClick: () => state.rotateImage(5), - text: '←R', - }, - { - className: styles.rotate_right_btn, - onClick: () => state.rotateImage(5), - text: 'R→', - }, - { - className: styles.apply_btn, - onClick: () => state.writeImage(), - text: '描画', - }, - ] satisfies { - className: string - onClick: () => void - text: string - }[] - return ( <> <Block title={'画像の選択'}> - <input + <Input type="file" onChange={e => { - state.upload(e.target.files) + state.current?.upload(e.target.files) window.location.hash = 'preview' }} /> @@ -83,14 +40,11 @@ export function IconMakerApp() { <Block title={'プレビュー'}> <p>位置を調整していい感じのところで描画を押してください。</p> - <p> - <b>既知のバグ:</b> ボタン操作をしないとつまみフレームが現れない - </p> <div> <div className="content"> <canvas - className="rich_image" - id="canvas-result" + ref={canvasRef} + className="tw-rounded-md tw-bg-trpfrog-50" style={{ width: '100%', maxWidth: '500px' }} /> </div> @@ -109,18 +63,18 @@ export function IconMakerApp() { <Block title={'生成した画像'}> <p>PCの方は右クリック、スマートフォンの方は長押しで保存できます。</p> <p> - <Image - src={'/icons_gallery/28'} - alt={'default image'} + <img + id="result-image" + src="https://res.cloudinary.com/trpfrog/icons_gallery/28" + alt="default image" width={500} height={500} - layout={'intrinsic'} /> </p> <p> - <Button externalLink={true} href={tweetLink}> + <MagicButton externalLink={true} href={tweetLink}> Tweet - </Button> + </MagicButton> </p> <p> (画像付きツイートで共有するのが無理だったので、一旦画像を保存してからこのボタンで共有して欲しいです〜(ごめんね)) diff --git a/src/lib/iconMaker.ts b/src/app/icon-maker/iconMaker.ts similarity index 56% rename from src/lib/iconMaker.ts rename to src/app/icon-maker/iconMaker.ts index 181fc454..fc26fad7 100644 --- a/src/lib/iconMaker.ts +++ b/src/app/icon-maker/iconMaker.ts @@ -1,8 +1,10 @@ +import { RefObject } from 'react' + export const ICON_SIZE = 500, CIRCLE_SIZE = 430 export class IconCanvas { - id: string + canvasRef: RefObject<HTMLCanvasElement> x: number = 35 y: number = 0 w: number = 0 @@ -14,18 +16,19 @@ export class IconCanvas { // @ts-ignore mask: HTMLImageElement - constructor(id: string) { - this.id = id + constructor(canvasRef: RefObject<HTMLCanvasElement>) { + this.canvasRef = canvasRef if (typeof window !== 'undefined') { this.faceImage = new Image() this.mask = new Image() } } - upload(files: FileList | null) { + async upload(files: FileList | null) { if (files == null) return + if (this.canvasRef.current == null) return - const canvas = document.getElementById(this.id) as HTMLCanvasElement + const canvas = this.canvasRef.current let context = canvas.getContext('2d') as CanvasRenderingContext2D let reader = new FileReader() @@ -34,35 +37,44 @@ export class IconCanvas { this.y = 10 this.w = this.h = this.angle = 0 - reader.onload = event => { - this.faceImage.src = event.target!.result as string - this.mask.src = '/images/icon_maker/mask.png' - this.faceImage.onload = () => { - if (this.faceImage.width < this.faceImage.height) { - let ratio = this.faceImage.height / this.faceImage.width - this.w = CIRCLE_SIZE - this.h = CIRCLE_SIZE * ratio - } else { - let ratio = this.faceImage.width / this.faceImage.height - this.w = CIRCLE_SIZE * ratio - this.h = CIRCLE_SIZE - } - - canvas.width = ICON_SIZE - canvas.height = ICON_SIZE - - context.save() - context.drawImage(this.faceImage, 35, 10, this.w, this.h) - context.restore() - - context.save() - context.drawImage(this.mask, 0, 0, ICON_SIZE, ICON_SIZE) - context.restore() - } - } - // @ts-ignore reader.readAsDataURL(files[0]) + const event = await new Promise<ProgressEvent<FileReader>>(resolve => { + reader.onload = e => resolve(e) + }) + + // wait for loading + await Promise.all([ + new Promise(resolve => { + this.faceImage.src = event.target!.result as string + this.faceImage.addEventListener('load', resolve) + }), + new Promise(resolve => { + this.mask.src = '/images/icon_maker/mask.png' + this.mask.addEventListener('load', resolve) + }), + ]) + + if (this.faceImage.width < this.faceImage.height) { + let ratio = this.faceImage.height / this.faceImage.width + this.w = CIRCLE_SIZE + this.h = CIRCLE_SIZE * ratio + } else { + let ratio = this.faceImage.width / this.faceImage.height + this.w = CIRCLE_SIZE * ratio + this.h = CIRCLE_SIZE + } + + canvas.width = ICON_SIZE + canvas.height = ICON_SIZE + + context.save() + context.drawImage(this.faceImage, 35, 10, this.w, this.h) + context.restore() + + context.save() + context.drawImage(this.mask, 0, 0, ICON_SIZE, ICON_SIZE) + context.restore() } moveImage(dx: number, dy: number) { @@ -87,9 +99,9 @@ export class IconCanvas { } applyCanvas() { - console.log(this.id) + if (this.canvasRef.current == null) return - const canvas = document.getElementById(this.id) as HTMLCanvasElement + const canvas = this.canvasRef.current let context = canvas.getContext('2d') as CanvasRenderingContext2D context.beginPath() @@ -111,7 +123,9 @@ export class IconCanvas { writeImage() { if (typeof window === 'undefined') return - const canvas = document.getElementById(this.id) as HTMLCanvasElement + if (this.canvasRef.current == null) return + + const canvas = this.canvasRef.current const resImg = canvas.toDataURL() const result = document.getElementById('result-image') as HTMLImageElement result.src = resImg diff --git a/src/app/icon-maker/iconMakerHooks.ts b/src/app/icon-maker/iconMakerHooks.ts new file mode 100644 index 00000000..1705ccdf --- /dev/null +++ b/src/app/icon-maker/iconMakerHooks.ts @@ -0,0 +1,70 @@ +import { MutableRefObject, RefObject, useMemo, useRef } from 'react' + +import { IconCanvas } from '@/app/icon-maker/iconMaker' +import styles from '@/app/icon-maker/style.module.scss' + +export function useIconMakerRef(imgRef: RefObject<HTMLCanvasElement>) { + const state = useRef<IconCanvas>() + if (!state.current) { + state.current = new IconCanvas(imgRef) + } + return state as MutableRefObject<IconCanvas> +} + +export function useIconMakerController(state: MutableRefObject<IconCanvas>) { + return useMemo( + () => + [ + { + className: styles.plus_btn, + onClick: () => state.current?.scaleImage(1.05), + text: '+', + }, + { + className: styles.minus_btn, + onClick: () => state.current?.scaleImage(1 / 1.05), + text: '-', + }, + { + className: styles.left_btn, + onClick: () => state.current?.moveImage(-5, 0), + text: '←', + }, + { + className: styles.down_btn, + onClick: () => state.current?.moveImage(0, 5), + text: '↓', + }, + { + className: styles.up_btn, + onClick: () => state.current?.moveImage(0, -5), + text: '↑', + }, + { + className: styles.right_btn, + onClick: () => state.current?.moveImage(5, 0), + text: '→', + }, + { + className: styles.rotate_left_btn, + onClick: () => state.current?.rotateImage(5), + text: '←R', + }, + { + className: styles.rotate_right_btn, + onClick: () => state.current?.rotateImage(5), + text: 'R→', + }, + { + className: styles.apply_btn, + onClick: () => state.current?.writeImage(), + text: '描画', + }, + ] satisfies { + className: string + onClick: () => void + text: string + }[], + [state], + ) +} diff --git a/src/app/icon-maker/page.tsx b/src/app/icon-maker/page.tsx index 75837c71..849b3657 100644 --- a/src/app/icon-maker/page.tsx +++ b/src/app/icon-maker/page.tsx @@ -15,7 +15,7 @@ export const metadata = { export default function Index() { return ( - <MainWrapper> + <MainWrapper gridLayout> <Title title={'アイコンメーカー.ts'} ribbonText={'BETA'} diff --git a/src/app/layout.module.scss b/src/app/layout.module.scss deleted file mode 100644 index f9d17390..00000000 --- a/src/app/layout.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -.body { -} - -.layout { - min-height: 100lvh; - width: 100%; - display: flex; - flex-direction: column; - & > main { - flex: 1; - } - & > * { - display: block; - } -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b2115b08..c5618852 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,14 @@ import { Suspense } from 'react' import * as React from 'react' -import '@/styles/globals.scss' +import '@/styles/variables.css' +import './globals.css' import 'react-loading-skeleton/dist/skeleton.css' import type { Metadata } from 'next' import { Viewport } from 'next' import { Toaster } from 'react-hot-toast' +import { tv } from 'tailwind-variants' import { Favicon } from '@/components/head/Favicon' import { BackToTop } from '@/components/organisms/BackToTop' @@ -20,8 +22,6 @@ import { JotaiProvider } from '@/components/utils/JotaiProvider' import { SITE_NAME } from '@/lib/constants' import { fontVariables } from '@/lib/googleFonts' -import styles from './layout.module.scss' - const siteName = SITE_NAME const description = 'さかなになりたいね' const productionURL = 'https://trpfrog.net' @@ -58,6 +58,14 @@ export const viewport: Viewport = { colorScheme: 'light dark', } +const styles = tv({ + slots: { + body: 'tw-scroll-smooth tw-bg-body-color tw-text-text-color print:tw-bg-white', + layout: 'tw-flex tw-min-h-screen tw-flex-col', + main: 'tw-flex-1', + }, +})() + type Props = { children: React.ReactNode } @@ -69,13 +77,13 @@ export default function RootLayout({ children }: Props) { <Favicon /> <FixTooLargeFontAwesomeIcons /> </head> - <body className={`${fontVariables} ${styles.body}`}> + <body className={styles.body({ className: fontVariables })}> <JotaiProvider> <Toaster /> - <div className={styles.layout}> + <div className={styles.layout()}> <Header /> <Navigation /> - <main>{children}</main> + <main className={styles.main()}>{children}</main> <Footer /> </div> <BackToTop /> diff --git a/src/app/legal/ReturnButton.tsx b/src/app/legal/ReturnButton.tsx index 8d50f799..8c04e075 100644 --- a/src/app/legal/ReturnButton.tsx +++ b/src/app/legal/ReturnButton.tsx @@ -4,7 +4,7 @@ import path from 'path' import { usePathname } from 'next/navigation' -import { Button } from '@/components/atoms/Button' +import { MagicButton } from 'src/components/atoms/MagicButton' export function ReturnButton() { const pathname = usePathname() @@ -13,6 +13,6 @@ export function ReturnButton() { if (basename.startsWith('legal')) { return <></> } else { - return <Button href={'/legal'}>戻る</Button> + return <MagicButton href={'/legal'}>戻る</MagicButton> } } diff --git a/src/app/legal/layout.tsx b/src/app/legal/layout.tsx index 05e2531b..e3353d7d 100644 --- a/src/app/legal/layout.tsx +++ b/src/app/legal/layout.tsx @@ -10,7 +10,7 @@ type Props = { export default function RootLayout({ children }: Props) { return ( - <MainWrapper> + <MainWrapper gridLayout> {children} <ReturnButton /> </MainWrapper> diff --git a/src/app/legal/page.mdx b/src/app/legal/page.mdx index b3224b67..f070d9d0 100644 --- a/src/app/legal/page.mdx +++ b/src/app/legal/page.mdx @@ -1,6 +1,6 @@ import { Title } from "@/components/organisms/Title"; import { Block } from "@/components/molecules/Block"; -import { Button } from "@/components/atoms/Button"; +import { MagicButton } from "@/components/atoms/MagicButton"; export const metadata = { title: 'Legal Information' @@ -12,7 +12,7 @@ export const metadata = { 以下のページでは当サイトでお預かりした個人情報の管理の方法について説明しています。 - <Button href={'/legal/privacy'}>プライバシーポリシー</Button> + <MagicButton href={'/legal/privacy'}>プライバシーポリシー</MagicButton> </Block> @@ -20,7 +20,7 @@ export const metadata = { 以下のページでは免責事項について説明しています。 - <Button href={'/legal/disclaimer'}>免責事項</Button> + <MagicButton href={'/legal/disclaimer'}>免責事項</MagicButton> </Block> @@ -28,6 +28,6 @@ export const metadata = { 以下のページでは当サイト上に掲載されたコンテンツの権利について説明しています。 - <Button href={'/legal/copyright'}>著作権について</Button> + <MagicButton href={'/legal/copyright'}>著作権について</MagicButton> </Block> diff --git a/src/app/links/MutualLinkBlock.tsx b/src/app/links/MutualLinkBlock.tsx index 538e09a6..8dcb0ba9 100644 --- a/src/app/links/MutualLinkBlock.tsx +++ b/src/app/links/MutualLinkBlock.tsx @@ -1,41 +1,56 @@ import * as React from 'react' -import styles from '@/app/(home)/page.module.scss' +import { faGithub, faTwitter } from '@fortawesome/free-brands-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Button } from '@/components/atoms/Button' -import { OpenInNewTab } from '@/components/atoms/OpenInNewTab' +import { PlainBlock } from '@/components/atoms/PlainBlock' +import { ServerLinkCard } from '@/components/organisms/LinkCard/ServerLinkCard' +import { A } from '@/components/wrappers' -import { calcMonospacedTextWidth } from '@/lib/utils' +import { tv } from '@/lib/tailwind/variants' import { MutualLinkRecord } from './loader' -export function MutualLinkBlock(props: { record: MutualLinkRecord }) { +const styles = tv({ + slots: { + card: 'tw-rounded-lg tw-p-4', + h3Wrapper: 'tw-mb-2 tw-flex tw-justify-between', + h3: 'tw-text-lg tw-font-bold', + social: 'tw-flex tw-gap-2', + twitter: 'tw-text-xl tw-text-[#1DA1F2] hover:tw-brightness-125', + github: 'tw-text-xl tw-text-[#333] hover:tw-text-gray-400', + }, +})() + +export async function MutualLinkBlock(props: { record: MutualLinkRecord }) { const { - record: { url, siteName, ownerName, twitterId, description }, + record: { url, ownerName, twitter, github }, } = props - // Shrink siteName if its length too long - const style = - calcMonospacedTextWidth(siteName) < 20 - ? {} - : ({ - letterSpacing: -0.5, - } as React.CSSProperties) - return ( - <div className={styles.link_block}> - <p style={{ textAlign: 'center' }}> - <Button externalLink={true} href={url} style={style}> - {siteName} - </Button> - </p> - <p> - <OpenInNewTab href={`https://twitter.com/${twitterId}/`}> - <b>{ownerName}</b> - </OpenInNewTab> - さんのHP - </p> - <p>{description}</p> - </div> + <PlainBlock className={styles.card()}> + <div className={styles.h3Wrapper()}> + <h3 className={styles.h3()}>{ownerName} さんのHP</h3> + <div className={styles.social()}> + {github && ( + <A + href={`https://github.com/${github}`} + className={styles.github()} + > + <FontAwesomeIcon icon={faGithub} /> + </A> + )} + {twitter && ( + <A + href={`https://twitter.com/${twitter}`} + className={styles.twitter()} + > + <FontAwesomeIcon icon={faTwitter} /> + </A> + )} + </div> + </div> + <ServerLinkCard href={url} /> + </PlainBlock> ) } diff --git a/src/app/links/fetchOGPImage.ts b/src/app/links/fetchOGPImage.ts deleted file mode 100644 index df966d00..00000000 --- a/src/app/links/fetchOGPImage.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { JSDOM } from 'jsdom' -import { LRUCache } from 'lru-cache' -import sharp from 'sharp' - -const cache = new LRUCache({ - max: 100, -}) - -export async function fetchOGPImageBase64(url: string) { - const ogpUrl = await fetch(url) - .then(res => res.text()) - .then(text => { - const el = new JSDOM(text) - const headEls = el.window.document.querySelectorAll('head > meta') - const ogImage = Array.from(headEls) - .find(v => { - const prop = v.getAttribute('property') - if (!prop) return false - return prop === 'og:image' - }) - ?.getAttribute('content') - return ogImage - }) - if (!ogpUrl) return null - const res = await fetch(ogpUrl) - const blob = await res.blob() - - // resize to 1200 x 630 px using sharp - const buffer = Buffer.from(await blob.arrayBuffer()) - const resizedBuffer = await sharp(buffer).resize(120, 63).toBuffer() - return resizedBuffer.toString('base64') -} diff --git a/src/app/links/loader.ts b/src/app/links/loader.ts index 33195ab8..5ac219dd 100644 --- a/src/app/links/loader.ts +++ b/src/app/links/loader.ts @@ -2,14 +2,17 @@ import fs from 'fs/promises' import path from 'path' import yaml from 'js-yaml' +import { z } from 'zod' -export type MutualLinkRecord = { - url: string - siteName: string - ownerName: string - twitterId: string - description: string -} +const MutualLinkRecordSchema = z.object({ + url: z.string(), + ownerName: z.string(), + twitter: z.string().optional(), + github: z.string().optional(), + description: z.string().optional(), +}) + +export type MutualLinkRecord = z.infer<typeof MutualLinkRecordSchema> export const loadMutualLinkRecords: () => Promise< MutualLinkRecord[] @@ -22,7 +25,7 @@ export const loadMutualLinkRecords: () => Promise< 'mutual_links.yaml', ) const yamlText = await fs.readFile(yamlPath, 'utf-8') - const links = yaml.load(yamlText) as MutualLinkRecord[] + const links = MutualLinkRecordSchema.array().parse(yaml.load(yamlText)) return links.sort(({ ownerName: a }, { ownerName: b }) => { if (a < b) { diff --git a/src/app/links/mutual_links.yaml b/src/app/links/mutual_links.yaml index 81113618..ba78b065 100644 --- a/src/app/links/mutual_links.yaml +++ b/src/app/links/mutual_links.yaml @@ -1,89 +1,90 @@ - url: https://azukibar.dev/ - siteName: アイスの棒 ownerName: あずきバー - twitterId: azukibar_D - description: 技術系ブログ + twitter: azukibar_D + github: Azuki-bar - url: https://asunarowasabi.github.io/html/github/docs/mypage.html - siteName: 翌檜郷 ownerName: あすなろわさび - twitterId: Asunarowasabi_U - description: 踏み逃げ推奨 + twitter: Asunarowasabi_U - url: https://uxhpu.net/ - siteName: あずきノート ownerName: あずきちゃん - twitterId: uxhpu - description: 力学と数学の解説記事など + twitter: uxhpu - url: https://www.kyu099.net/ - siteName: ウサギ小屋 ownerName: きゅ〜 - twitterId: kyu_099 - description: 徒歩ブログになる予定 + twitter: kyu_099 + github: kyu099 - url: https://gotti.dev/ - siteName: ごっちでぶ ownerName: gotti - twitterId: _nil_a_ - description: 技術と旅行と馬券の記事など + twitter: _nil_a_ + github: gotti - url: https://negiissei.com/ - siteName: ねぎー日記 ownerName: ねぎ一世 - twitterId: negiissei - description: 音楽をつくるオタク + twitter: negiissei + github: negiissei - url: https://hutinoatari.dev/ - siteName: 捻れたバベル ownerName: 淵野アタリ - twitterId: ebioishii_u - description: 手芸とうどん + twitter: ebioishii_u - url: https://baki-0.github.io/index.html - siteName: B.B.B.World ownerName: B - twitterId: uec19b - description: BBBBBBBBBBBBBB + twitter: uec19b + github: Baki-0 - url: https://medit.link/ - siteName: meditのWebサイト ownerName: medit - twitterId: meditq - description: 面白ツールやサークルなど + twitter: meditq - url: https://www.mbsoftware.tokyo/ - siteName: Monbrand Software ownerName: おひげP - twitterId: uec_hige - description: プロフィールのサイト + twitter: uec_hige - url: https://mato1370.net/ - siteName: マトーサンの部屋 ownerName: マトーサン - twitterId: mato1370 - description: オタクに無理やりサイトを作らされててウケた(怖い) + twitter: mato1370 - url: https://ruuu.dev/ - siteName: るーどっとでぶ ownerName: るー - twitterId: ruu_uec - description: プロフィールのサイト + twitter: ruu_uec - url: https://mocchan.dev/ - siteName: Lunatic電通生もっちゃんの部屋 ownerName: もっちゃん - twitterId: sakuramochi0708 - description: JKセンターの利用開始手順の解説など + twitter: sakuramochi0708 + github: sakuramochi708 - url: https://www.mma.club.uec.ac.jp/~terry/ - siteName: Terry's Blog ownerName: Terry - twitterId: terry_univ - description: 技術系ブログ + twitter: terry_univ - url: https://azumabashi.dev/ - siteName: Azumabashi ownerName: Azumabashi - twitterId: azm_bashi - description: 自作 Web App の紹介など + twitter: azm_bashi + +- url: https://yuino.dev/ + ownerName: ゆい + twitter: yui__yuuki + github: Yuki-Yui + +- url: https://helkun.dev/ + ownerName: へるくん + twitter: helkun + github: hel-kun + +- url: https://sushichan.live + ownerName: すし + twitter: sushi_chan_sub + github: sushi-chaaaan + +- url: https://lnln.dev/ + ownerName: りんりん + twitter: lnln_ch + github: ybasviel + +- url: https://minofumino.net/ + ownerName: ふみ + twitter: fmnpt + github: minofumino diff --git a/src/app/links/page.tsx b/src/app/links/page.tsx index b4974c36..152dc587 100644 --- a/src/app/links/page.tsx +++ b/src/app/links/page.tsx @@ -1,9 +1,6 @@ import { Metadata } from 'next' -import styles from '@/app/(home)/page.module.scss' - import { MainWrapper } from '@/components/atoms/MainWrapper' -import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' import { loadMutualLinkRecords, MutualLinkRecord } from './loader' @@ -11,26 +8,24 @@ import { MutualLinkBlock } from './MutualLinkBlock' export const metadata = { title: '相互リンク', - description: 'オタク各位のWebサイトです。', + description: '知人の個人サイト紹介', } satisfies Metadata export default async function Index() { const mutualLinks: MutualLinkRecord[] = await loadMutualLinkRecords() return ( - <MainWrapper> + <MainWrapper gridLayout> <Title title={metadata.title} description={metadata.description}> <p> 順番はハンドルネームをUTF-8でソートしたもの。 <s>片想いリンクになったやつもある</s> </p> - -
- {mutualLinks.map(record => ( - - ))} -
-
+
+ {mutualLinks.map(record => ( + + ))} +
) } diff --git a/src/app/loading.tsx b/src/app/loading.tsx index d4446d87..defff65c 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -3,7 +3,7 @@ import { LoadingBlock } from '@/components/molecules/LoadingBlock' export default function Loading() { return ( - + ) diff --git a/src/app/music/layout.tsx b/src/app/music/layout.tsx index a190aaeb..f557cd11 100644 --- a/src/app/music/layout.tsx +++ b/src/app/music/layout.tsx @@ -14,5 +14,5 @@ type Props = { } export default function RootLayout({ children }: Props) { - return {children} + return {children} } diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index 722093b4..04f72a1e 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -1,9 +1,10 @@ -import Image from 'next/legacy/image' - -import { Button } from '@/components/atoms/Button' +import { Image } from '@/components/atoms/Image' +import { InlineLink } from '@/components/atoms/InlineLink' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' -import { LiteYouTubeEmbedWrapper } from '@/components/utils/LiteYouTubeEmbedWrapper' +import { YouTube } from '@/components/organisms/YouTube' + +import { MagicButton } from 'src/components/atoms/MagicButton' import Lyrics from './Lyrics.mdx' import styles from './style.module.scss' @@ -13,25 +14,25 @@ export default function Music() { <> <p> - ねぎ一世(<a href="https://twitter.com/negiissei">@negiissei</a>)さんに - 「<b>つまみのうた</b>」を作っていただきました!(????) + ねぎ一世( + <InlineLink href="https://twitter.com/negiissei"> + @negiissei + </InlineLink> + )さんに 「<b>つまみのうた</b>」を作っていただきました!(????) ありがとうございます!!! </p> <p> - <Button externalLink={true} href="https://linkco.re/N4Z8hdvX"> + <MagicButton externalLink={true} href="https://linkco.re/N4Z8hdvX"> 購入・ストリーミング - </Button> - </p> - <p> - <Image - src={'musicbanner'} - className={'rich_image'} - width={'500'} - height={'100'} - layout={'responsive'} - alt={'つまみのうたのバナー'} - /> + </MagicButton> </p> + <Image + src={'musicbanner'} + className="tw-my-4" + width={500} + height={100} + alt={'つまみのうたのバナー'} + /> @@ -44,15 +45,17 @@ export default function Music() { 「うたスキ」「うたスキ動画」の両方に対応した店舗で歌えるらしいので、カラオケ行く人はよろしくお願いします。 僕は歌いません。(?)

-
- - +
@@ -64,20 +67,16 @@ export default function Music() {

各種音楽配信サイトで配信中!

- +

YouTubeでも公開中!

-
- -
+

作詞作曲:ねぎ一世

-
- -
+
) diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index f6cae7ef..9b9060e2 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,15 +1,16 @@ import Image from 'next/legacy/image' -import { Button } from '@/components/atoms/Button' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Title } from '@/components/organisms/Title' +import { MagicButton } from 'src/components/atoms/MagicButton' + export default function NotFound() { const IB = ({ children }: any) => ( {children} ) return ( - + リンクが誤っている可能性があります。</IB> </p> <p> - <Button href={'/'}>トップページに戻る</Button> + <MagicButton href={'/'}>トップページに戻る</MagicButton> </p> </div> diff --git a/src/app/tweets/PageNavigation/index.module.scss b/src/app/tweets/PageNavigation/index.module.scss index 17780185..19ca3a3f 100644 --- a/src/app/tweets/PageNavigation/index.module.scss +++ b/src/app/tweets/PageNavigation/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../styles/mixins'; .button { background-color: #fff; diff --git a/src/app/tweets/SearchForm/index.tsx b/src/app/tweets/SearchForm/index.tsx index e8765ba7..9fe8c530 100644 --- a/src/app/tweets/SearchForm/index.tsx +++ b/src/app/tweets/SearchForm/index.tsx @@ -1,16 +1,18 @@ 'use client' +import { Input } from '@/components/wrappers' + import styles from './index.module.scss' export function SearchForm(props: { defaultValue?: string }) { return (
- - +
) } diff --git a/src/app/tweets/TweetCard/index.module.scss b/src/app/tweets/TweetCard/index.module.scss index 7cd429f4..53d64303 100644 --- a/src/app/tweets/TweetCard/index.module.scss +++ b/src/app/tweets/TweetCard/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../styles/mixins'; .retweet { margin: 0; diff --git a/src/app/tweets/TweetCard/index.tsx b/src/app/tweets/TweetCard/index.tsx index fce234f1..070beff5 100644 --- a/src/app/tweets/TweetCard/index.tsx +++ b/src/app/tweets/TweetCard/index.tsx @@ -5,8 +5,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import dayjs from 'dayjs' import reactStringReplace from 'react-string-replace' -import { OpenInNewTab } from '@/components/atoms/OpenInNewTab' +import { InlineLink } from '@/components/atoms/InlineLink' import { PlainBlock } from '@/components/atoms/PlainBlock' +import { A } from '@/components/wrappers' import styles from './index.module.scss' @@ -28,12 +29,12 @@ function ScreenNameLink(props: { children: React.ReactNode }) { return ( - {props.children} - + ) } @@ -48,32 +49,32 @@ function TweetString({ text, /(https?:\/\/[^\s\n ]+)/g, (match, i) => ( - + {match} - + ), ) replaced = reactStringReplace(replaced, /\B@([\w_]+)/g, (match, i) => ( - @{match} - + )) replaced = reactStringReplace( replaced, /\B#([^\s\n「」()#]+)/g, (match, i) => ( - #{match} - + ), ) @@ -109,9 +110,9 @@ export function DateCard({ date }: { date: Date }) { return (

- + {dayjs(date).format('YYYY年M月D日')} - +

) @@ -224,9 +225,9 @@ export function TweetCard({
) : photos > 0 ? ( - + View pictures on{' '} - twitter.com! + twitter.com! ) : ( @@ -235,10 +236,10 @@ export function TweetCard({ )}
diff --git a/src/app/tweets/page.tsx b/src/app/tweets/page.tsx index cbd7be4a..32a900ec 100644 --- a/src/app/tweets/page.tsx +++ b/src/app/tweets/page.tsx @@ -2,10 +2,12 @@ import { Metadata } from 'next' import dayjs from 'dayjs' -import { Button } from '@/components/atoms/Button' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' +import { Li, UnorderedList } from '@/components/wrappers' + +import { MagicButton } from 'src/components/atoms/MagicButton' import { SearchForm } from './SearchForm' import { TweetArea } from './TweetArea' @@ -28,7 +30,7 @@ export default async function Index({ searchParams }: any) { const oneYearsAgo = dayjs(new Date()).subtract(1, 'year').format('YYYY-MM-DD') return ( - + <p> つまみさんの過去ツイデータベースです。 @@ -41,7 +43,7 @@ export default async function Index({ searchParams }: any) { だいたい数年前のツイートは自分でも「何言ってんだこいつ……」となることが多いです。 </p> <p> - <Button + <MagicButton href={ '/tweets?q=' + encodeURIComponent('date:' + oneYearsAgo) + @@ -49,21 +51,21 @@ export default async function Index({ searchParams }: any) { } > 1年前のツイートを見る - </Button> + </MagicButton> </p> <br /> <details> <summary>実装済みの機能</summary> - <ul style={{ marginTop: 0 }}> - <li>AND 検索</li> - <li>since/until 検索 (日付のみ e.g. 2000-10-17)</li> - <li>date 検索 (特定の日のツイートを検索します)</li> - <li>min_faves/max_faves 検索</li> - <li>min_retweets/max_retweets 検索</li> - <li>from 検索</li> - <li>order:asc で古い順に並び替え</li> - <li>マイナス検索 (上記のいずれにも使用可)</li> - </ul> + <UnorderedList style={{ marginTop: 0 }}> + <Li>AND 検索</Li> + <Li>since/until 検索 (日付のみ e.g. 2000-10-17)</Li> + <Li>date 検索 (特定の日のツイートを検索します)</Li> + <Li>min_faves/max_faves 検索</Li> + <Li>min_retweets/max_retweets 検索</Li> + <Li>from 検索</Li> + <Li>order:asc で古い順に並び替え</Li> + <Li>マイナス検索 (上記のいずれにも使用可)</Li> + </UnorderedList> </details> <SearchForm defaultValue={searchParams.q} /> diff --git a/src/app/walking/page.tsx b/src/app/walking/page.tsx index d7ed6730..39128afc 100644 --- a/src/app/walking/page.tsx +++ b/src/app/walking/page.tsx @@ -21,13 +21,11 @@ export default async function Index() { const articles = await retrieveSortedBlogPostList(tag) return ( - + <Block title={'新着徒歩記事'}> <p> つまみログに書いた「<b>徒歩</b>」タグの新着記事です。 - その他の徒歩記事は <a href={'https://walk.trpfrog.net'}>WALKICLES</a>{' '} - をご覧ください。 </p> </Block> <ArticleGrid> diff --git a/src/app/works/Keywords.tsx b/src/app/works/Keywords.tsx new file mode 100644 index 00000000..5db3430b --- /dev/null +++ b/src/app/works/Keywords.tsx @@ -0,0 +1,49 @@ +import { Devicon, hasDevicon } from '@/components/atoms/Devicon' + +import { tv } from '@/lib/tailwind/variants' + +const styles = tv({ + slots: { + wrapper: '', + keywords: 'tw-flex tw-flex-wrap tw-gap-2', + keyword: [ + 'tw-inline-block tw-rounded-full tw-border-2 tw-border-trpfrog-200', + 'tw-px-3 tw-py-1 tw-text-sm tw-font-bold', + ], + title: 'tw-mb-1 tw-text-sm tw-text-gray-500', + }, +})() + +interface Props { + keywords: string[] +} + +export function Keywords({ keywords }: Props) { + return ( + <div className={styles.wrapper()}> + <h3 className={styles.title()}>TECHNOLOGIES</h3> + <ul className={styles.keywords()}> + {keywords.map(k => + hasDevicon(k) ? ( + <li key={k} className={styles.keyword()}> + <Devicon + iconName={k} + className="tw-relative tw-top-[1px] tw-mr-1 tw-hidden dark:tw-inline" + /> + <Devicon + iconName={k} + colored + className="tw-relative tw-top-[1px] tw-mr-1 dark:tw-hidden" + /> + {k} + </li> + ) : ( + <li key={k} className={styles.keyword()}> + {k} + </li> + ), + )} + </ul> + </div> + ) +} diff --git a/src/app/works/contents/clover-bridge.md b/src/app/works/contents/clover-bridge.md index ff07ecaa..2b0cd9d7 100644 --- a/src/app/works/contents/clover-bridge.md +++ b/src/app/works/contents/clover-bridge.md @@ -8,7 +8,7 @@ keywords: - OpenGL - C++ - Computer Graphics -date: 2021/12/1 +date: 2021-12-01 links: GitHub: https://github.com/TrpFrog/clover-bridge --- diff --git a/src/app/works/contents/cookie-animation.md b/src/app/works/contents/cookie-animation.md index 532c2f3d..4a951e24 100644 --- a/src/app/works/contents/cookie-animation.md +++ b/src/app/works/contents/cookie-animation.md @@ -6,7 +6,7 @@ image: height: 300 keywords: - C programming -date: 2020/2/12 +date: 2020-02-12 links: GitHub: https://github.com/TrpFrog/CookieAnimation アニメーション (10.7MB): https://github.com/TrpFrog/CookieAnimation/blob/master/anim.gif diff --git a/src/app/works/contents/elements-learning.md b/src/app/works/contents/elements-learning.md index e29be227..b6aa4620 100644 --- a/src/app/works/contents/elements-learning.md +++ b/src/app/works/contents/elements-learning.md @@ -9,7 +9,7 @@ keywords: - CSS - JavaScript - Cookie -date: 2021/6/15 +date: 2021-06-15 links: GitHub: https://github.com/TrpFrog/elements-learning Webサイト: https://trpfrog.github.io/elements-learning/ diff --git a/src/app/works/contents/frogrobo-2nd.md b/src/app/works/contents/frogrobo-2nd.md index 26b8bfee..46c0d1ec 100644 --- a/src/app/works/contents/frogrobo-2nd.md +++ b/src/app/works/contents/frogrobo-2nd.md @@ -10,7 +10,7 @@ keywords: - Google Cloud - Deep Learning - NLP -date: 2023/1/17 +date: 2023-01-17 links: GitHub: https://github.com/TrpFrog/FrogRobo つまみロボ (停止中): https://twitter.com/FrogRobo diff --git a/src/app/works/contents/frogrobo.md b/src/app/works/contents/frogrobo.md index 38f7ff28..287c8529 100644 --- a/src/app/works/contents/frogrobo.md +++ b/src/app/works/contents/frogrobo.md @@ -5,7 +5,7 @@ keywords: - Java - Twitter API - Twitter4J -date: 2016/4/26 +date: 2016-04-26 links: GitHub: https://github.com/TrpFrog/old-FrogRobo つまみロボ (停止中): https://twitter.com/FrogRobo diff --git a/src/app/works/contents/jellyfish-aquarium.md b/src/app/works/contents/jellyfish-aquarium.md index 949fe682..3d571dd7 100644 --- a/src/app/works/contents/jellyfish-aquarium.md +++ b/src/app/works/contents/jellyfish-aquarium.md @@ -7,7 +7,7 @@ image: keywords: - Processing - Computer Graphics -date: 2021/8/23 +date: 2021-08-23 links: GitHub: https://github.com/TrpFrog/jellyfish-aquarium --- diff --git a/src/app/works/contents/knock-on-gpus.md b/src/app/works/contents/knock-on-gpus.md new file mode 100644 index 00000000..56e09ff8 --- /dev/null +++ b/src/app/works/contents/knock-on-gpus.md @@ -0,0 +1,46 @@ +--- +title: knock-on-gpus +subtitle: GPU の空き状況を確認してから次のコマンドを実行するツール +id: knock-on-gpus +image: + path: works/knock-on-gpus + width: 1680 + height: 1266 +keywords: + - Rust + - NVML + - Maturin + - GitHub Actions +date: 2024-02-19 +links: + GitHub: https://github.com/trpfrog/knock-on-gpus + PyPI: https://pypi.org/project/knock-on-gpus/ +--- + +GPU の空き状況を確認してから次のコマンドを実行する CLI ツール。Rust 製。 + +共用のGPUマシン (複数枚刺さっている想定) で学習コード回す前に + +``` +knock-on-gpus -- python train.py +``` + +とコマンドを挟んでやることで、既に使用中のGPUを間違って占拠してしまう事故を防ぐことができます。 + +オプションを使うと細かい設定もできて、 + +- `knock-on-gpus --devices 0,1,2`: 0, 1, 2 番の GPU を選ぶ +- `knock-on-gpus --auto-select 2`: 空いている GPU から 2 つを選ぶ +- `knock-on-gpus --d 0,1,2 -a 2`: 0, 1, 2 番の中から空いている GPU から 2 つを選ぶ + +という感じで使えます。`--` でコマンドを繋ぐと、空いている GPU があった場合にのみコマンドを実行します。 +使用する GPU は環境変数 `CUDA_VISIBLE_DEVICES` にセットする形で指定されます。 + +PyPI で公開しているので、`pip` でインストールできます。 + +``` +pip install knock-on-gpus +``` + +ビルドには Maturin を使って wheel を作っています。 +これは GitHub Actions にやらせて、自動でそのまま PyPI にアップロードさせています。 diff --git a/src/app/works/contents/one-hour-reversi.md b/src/app/works/contents/one-hour-reversi.md index 83e00a85..089628ee 100644 --- a/src/app/works/contents/one-hour-reversi.md +++ b/src/app/works/contents/one-hour-reversi.md @@ -7,7 +7,7 @@ image: keywords: - Java - Swing -date: 2020/12/3 +date: 2020-12-03 links: Gist: https://gist.github.com/TrpFrog/bcdfcc015272ec71d38208be7829bf64 動画: https://twitter.com/TrpFrog/status/1334487946150330369 diff --git a/src/app/works/contents/otaku-discord.md b/src/app/works/contents/otaku-discord.md index bbab1b70..72b8b1c4 100644 --- a/src/app/works/contents/otaku-discord.md +++ b/src/app/works/contents/otaku-discord.md @@ -9,7 +9,7 @@ keywords: - Python - Discord API - JavaScript -date: 2021/12/16 +date: 2021-12-16 links: GitHub: https://github.com/TrpFrog/otaku-channels Webサイト: https://otaku-discord.trpfrog.net diff --git a/src/app/works/contents/ppo-icm-montaincar.md b/src/app/works/contents/ppo-icm-montaincar.md index 2364ccfe..4d694be6 100644 --- a/src/app/works/contents/ppo-icm-montaincar.md +++ b/src/app/works/contents/ppo-icm-montaincar.md @@ -9,7 +9,7 @@ keywords: - PyTorch - Deep Learning - Reinforcement Learning -date: 2023/3/23 +date: 2023-03-23 links: GitHub: https://github.com/TrpFrog/ppo-icm-mountaincar 発表スライド: https://media.trpfrog.net/univ/lab/ppo-icm.pdf diff --git a/src/app/works/contents/space-wandering.md b/src/app/works/contents/space-wandering.md index 984cfd6c..8f449af8 100644 --- a/src/app/works/contents/space-wandering.md +++ b/src/app/works/contents/space-wandering.md @@ -7,7 +7,7 @@ image: keywords: - Java - Swing -date: 2021/2/24 +date: 2021-02-24 links: GitHub: https://github.com/TrpFrog/medipro-game 公式サイト: https://trpfrog.github.io/medipro-game diff --git a/src/app/works/contents/timetable-page.md b/src/app/works/contents/timetable-page.md index 3c24127a..2d7f87b9 100644 --- a/src/app/works/contents/timetable-page.md +++ b/src/app/works/contents/timetable-page.md @@ -9,7 +9,7 @@ keywords: - CSS - JavaScript - Docker -date: 2021/10/5 +date: 2021-10-05 links: GitHub: https://github.com/TrpFrog/timetable-page GitHub Packages: https://github.com/TrpFrog/timetable-page/pkgs/container/timetable diff --git a/src/app/works/contents/trpfrog-net-1st.md b/src/app/works/contents/trpfrog-net-1st.md index 6664cea5..94ffa307 100644 --- a/src/app/works/contents/trpfrog-net-1st.md +++ b/src/app/works/contents/trpfrog-net-1st.md @@ -7,7 +7,7 @@ image: keywords: - HTML - CSS -date: 2019/6/15 +date: 2019-06-15 links: GitHub: https://github.com/TrpFrog/old.trpfrog.net/tree/5bca766b93b6070760cdf96f25c47b114b9ea73b --- diff --git a/src/app/works/contents/trpfrog-net-2nd.md b/src/app/works/contents/trpfrog-net-2nd.md index 2b6feacc..5c964afe 100644 --- a/src/app/works/contents/trpfrog-net-2nd.md +++ b/src/app/works/contents/trpfrog-net-2nd.md @@ -8,7 +8,7 @@ keywords: - HTML - CSS - JavaScript -date: 2019/7/13 +date: 2019-07-13 links: GitHub: https://github.com/TrpFrog/old.trpfrog.net/commit/9ab453ac255a010efdb593ef1a9d92930b9d5f2e --- diff --git a/src/app/works/contents/trpfrog-net-3rd.md b/src/app/works/contents/trpfrog-net-3rd.md index 06e85938..257ef6af 100644 --- a/src/app/works/contents/trpfrog-net-3rd.md +++ b/src/app/works/contents/trpfrog-net-3rd.md @@ -9,7 +9,7 @@ keywords: - CSS - JavaScript - Python -date: 2020/2/28 +date: 2020-02-28 links: GitHub: https://github.com/TrpFrog/old.trpfrog.net Webサイト: https://old.trpfrog.net diff --git a/src/app/works/contents/trpfrog-net-4th.md b/src/app/works/contents/trpfrog-net-4th.md index 36fb1d4a..0e31db48 100644 --- a/src/app/works/contents/trpfrog-net-4th.md +++ b/src/app/works/contents/trpfrog-net-4th.md @@ -9,7 +9,7 @@ keywords: - Next.js - Node.js - SCSS -date: 2022/1/3 +date: 2022-01-03 links: GitHub: https://github.com/TrpFrog/trpfrog.net Webサイト (ここ): https://trpfrog.net diff --git a/src/app/works/contents/twitter-screen.md b/src/app/works/contents/twitter-screen.md index de009125..7c3d0a8b 100644 --- a/src/app/works/contents/twitter-screen.md +++ b/src/app/works/contents/twitter-screen.md @@ -8,7 +8,7 @@ keywords: - Java - Swing - Twitter API -date: 2020/11/23 +date: 2020-11-23 links: GitHub (現在非公開): https://github.com/TrpFrog/twitter-screen ブログ記事: https://trpfrog.hateblo.jp/entry/twitter-screen diff --git a/src/app/works/contents/uec-fulfilled.md b/src/app/works/contents/uec-fulfilled.md new file mode 100644 index 00000000..cad3a153 --- /dev/null +++ b/src/app/works/contents/uec-fulfilled.md @@ -0,0 +1,31 @@ +--- +title: uec-fulfilled +subtitle: 修得単位数が卒業条件を満たしているかを一発で確認するブックマークレット +id: uec-fulfilled +image: + path: works/uec-fulfilled + width: 1510 + height: 956 +keywords: + - Bookmarklet + - TypeScript + - Bun + - GitHub Pages + - GitHub Actions +date: 2024-02-20 +links: + GitHub: https://github.com/trpfrog/uec-fulfilled +--- + +学務情報システム上で確認できる修得単位数が、卒業条件を満たしているかを一発で確認するブックマークレットです。 + +学務情報システムの成績一覧ページでブックマークレットを実行すると、修得単位数のテーブルに残り単位数が表示されます。 +所要単位数を満たしている場合はチェックマークが表示され、その行が緑色で塗られます。 + +基本的には JavaScript の DOM API で学務情報システムのページを操作してテーブルを書き換えます。 +単純にブックマークレットを書くと開発体験が最悪なので、もとのコードは TypeScript でモジュールに分けて書き、それを Bun (`Bun.build`) でバンドルしました。 +バンドルは GitHub Actions で行っています。 + +ブックマークレットのためのスクリプトは GitHub Pages でホストし、ユーザはブックマークレット経由で +GitHub Pages 上のスクリプトを fetch して実行します。これにより、機能追加・バグ修正時にユーザ側での更新作業が不要になります。 + diff --git a/src/app/works/page.tsx b/src/app/works/page.tsx index a9295666..e69be3a1 100644 --- a/src/app/works/page.tsx +++ b/src/app/works/page.tsx @@ -2,50 +2,22 @@ import path from 'path' import { Metadata } from 'next' -import Image from 'next/legacy/image' -import ReactMarkdown from 'react-markdown' +import dayjs from 'dayjs' +import { MDXRemote } from 'next-mdx-remote/rsc' -import { Button } from '@/components/atoms/Button' +import { Image } from '@/components/atoms/Image' import { MainWrapper } from '@/components/atoms/MainWrapper' import { Block } from '@/components/molecules/Block' import { Title } from '@/components/organisms/Title' import { readMarkdowns } from '@/lib/mdLoader' -import styles from './style.module.scss' +import { getMarkdownOptions } from '@blog/_renderer/rendererProperties' -type KeywordsProps = { - keywords: string[] -} - -function Keywords({ keywords }: KeywordsProps) { - return ( - <p className={styles.keywords}> - <span className={styles.keyword_title}>TECHNOLOGIES</span> - <br /> - {keywords.map(k => ( - <span key={k} className={styles.keyword}> - {k} - </span> - ))} - </p> - ) -} +import { MagicButton } from 'src/components/atoms/MagicButton' -type Frontmatter = { - title: string - h2icon?: string - image?: { - path: string - width: number - height: number - } - keywords?: string[] - links?: { - [key: string]: string - } - date: `${number}/${number}/${number}` -} +import { Keywords } from './Keywords' +import { WorksFrontmatterSchema } from './schema' export const metadata = { title: 'Works', @@ -55,12 +27,13 @@ export const metadata = { export default async function Index() { // load all md files under /app/works/contents/*.md - const contents = await readMarkdowns<Frontmatter>( + const contents = await readMarkdowns( path.join(process.cwd(), 'src', 'app', 'works', 'contents'), + WorksFrontmatterSchema, ) return ( - <MainWrapper> + <MainWrapper gridLayout> <Title title={metadata.title} description={metadata.description}> <p>最終更新: 2023/5/31</p> @@ -73,33 +46,31 @@ export default async function Index() { h2icon={metadata.h2icon ?? 'trpfrog'} > {metadata.image && ( -
- {metadata.title -
+ {metadata.title )} {metadata.keywords && }

- Released: {metadata.date} + Released: {dayjs(metadata.date).format('YYYY-MM-DD')}{' '}

- {content} +

{Object.entries(metadata.links ?? {}).map(([linkTxt, url]) => { return ( - + ) })}

diff --git a/src/app/works/schema.ts b/src/app/works/schema.ts new file mode 100644 index 00000000..56d46210 --- /dev/null +++ b/src/app/works/schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +export const WorksFrontmatterSchema = z.object({ + title: z.string(), + subtitle: z.string().optional(), + h2icon: z.string().optional(), + image: z + .object({ + path: z.string(), + width: z.number(), + height: z.number(), + }) + .optional(), + keywords: z.array(z.string()).optional(), + links: z.record(z.string()).optional(), + date: z.coerce.date(), +}) + +export type WorksFrontmatter = z.infer diff --git a/src/app/works/style.module.scss b/src/app/works/style.module.scss deleted file mode 100644 index 1fc390af..00000000 --- a/src/app/works/style.module.scss +++ /dev/null @@ -1,28 +0,0 @@ -.keyword_title { - display: inline-block; - color: gray; - font-size: 0.8em; -} - -.keyword { - display: inline-block; - border: solid 2px #90e200; - border-radius: 1000px; - padding: 0 10px; - margin-right: 5px; - margin-bottom: 5px; - font-weight: bold; -} - -.hero_image { - position: relative; - overflow: hidden; - margin-top: 20px; - text-align: center; - width: 100%; - - img { - border-radius: 10px; - border: 1px solid rgb(lightgray, 0.3) !important; - } -} diff --git a/src/components/atoms/AnglePicker/index.module.scss b/src/components/atoms/AnglePicker/index.module.scss index 001da7b6..aa7f6e17 100644 --- a/src/components/atoms/AnglePicker/index.module.scss +++ b/src/components/atoms/AnglePicker/index.module.scss @@ -15,21 +15,21 @@ border-radius: 50%; box-shadow: 0 0 30px 5px rgba(black, 0.3) inset; - $thickness: 6deg; - $scales: 8; - $length: 45%; + --thickness: 6deg; + --scales: 8; + --length: 45%; background: radial-gradient( circle at 50% 50%, white 0%, - white (100% - $length), - transparent (100% - $length), + white calc(100% - var(--length)), + transparent calc(100% - var(--length)), transparent 100% ), repeating-conic-gradient( - from (-$thickness / 2) at 50% 50%, - rgba(105, 105, 105, 0.75) 0deg $thickness, - transparent $thickness (360deg / $scales) + from calc(-1 * var(--thickness) / 2) at 50% 50%, + rgba(105, 105, 105, 0.75) 0deg var(--thickness), + transparent var(--thickness) calc(360deg / var(--scales)) ), white; } @@ -48,21 +48,21 @@ .hand_dot { position: absolute; - $size: 18%; - top: -$size / 2; - left: 50% - $size / 2; - width: $size; - height: $size; + --size: 18%; + top: calc(-1 * var(--size) / 2); + left: calc(50% - var(--size) / 2); + width: var(--size); + height: var(--size); border-radius: 50%; background: var(--hand-color); } .hand { position: absolute; - $thickness: 5%; + --thickness: 5%; top: 0; - left: 50% - $thickness / 2; - width: $thickness; + left: calc(50% - var(--thickness) / 2); + width: var(--thickness); height: 50%; background: var(--hand-color); } @@ -71,12 +71,12 @@ position: absolute; width: 100%; height: 100%; - $size: 8%; + --size: 8%; background: radial-gradient( circle at 50% 50%, var(--hand-color) 0%, - var(--hand-color) $size, - transparent $size, + var(--hand-color) var(--size), + transparent var(--size), transparent 100% ); } diff --git a/src/components/atoms/Details/index.module.scss b/src/components/atoms/Details/index.module.scss index ff91a0fd..50c09c5b 100644 --- a/src/components/atoms/Details/index.module.scss +++ b/src/components/atoms/Details/index.module.scss @@ -19,7 +19,6 @@ } &::after { - font-family: var(--font-noto-sans-jp); transition: 100ms; font-weight: bold; color: var(--base-font-color); diff --git a/src/components/atoms/Devicon.tsx b/src/components/atoms/Devicon.tsx new file mode 100644 index 00000000..1e140bce --- /dev/null +++ b/src/components/atoms/Devicon.tsx @@ -0,0 +1,56 @@ +import React from 'react' + +import 'devicon' +import { twMerge } from '@/lib/tailwind/merge' + +export const deviconMap = { + Python: 'devicon-python-plain', + PyTorch: 'devicon-pytorch-original', + TypeScript: 'devicon-typescript-plain', + 'Google Cloud': 'devicon-googlecloud-plain', + 'Twitter API': 'devicon-twitter-original', + Twitter4J: 'devicon-twitter-original', + React: 'devicon-react-original', + 'Next.js': 'devicon-nextjs-plain', + 'Node.js': 'devicon-nodejs-plain', + JavaScript: 'devicon-javascript-plain', + OpenGL: 'devicon-opengl-plain', + 'C++': 'devicon-cplusplus-plain', + HTML: 'devicon-html5-plain', + CSS: 'devicon-css3-plain', + Docker: 'devicon-docker-plain', + Processing: 'devicon-processing-plain', + Java: 'devicon-java-plain', + 'C programming': 'devicon-c-plain', + SCSS: 'devicon-sass-original', + Rust: 'devicon-rust-plain', + Bun: 'devicon-bun-plain', + 'GitHub Actions': 'devicon-githubactions-plain', + 'GitHub Pages': 'devicon-github-plain', +} + +export type DeviconKey = keyof typeof deviconMap + +export function hasDevicon(iconName: string): iconName is DeviconKey { + return iconName in deviconMap +} + +export type DeviconProps = { + iconName: DeviconKey + colored?: boolean + className?: string + style?: React.CSSProperties +} + +export function Devicon(props: DeviconProps) { + return ( + + ) +} diff --git a/src/components/atoms/H2/index.module.scss b/src/components/atoms/H2/index.module.scss deleted file mode 100644 index 6a126668..00000000 --- a/src/components/atoms/H2/index.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.h2 { - display: flex; - align-items: center; -} - -.icon { - height: 1.5em; - margin-right: 5px; - vertical-align: baseline; -} diff --git a/src/components/atoms/H2/index.stories.tsx b/src/components/atoms/H2/index.stories.tsx deleted file mode 100644 index 5f45b15d..00000000 --- a/src/components/atoms/H2/index.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { iconURLs, H2 } from '.' - -import type { Meta, StoryObj } from '@storybook/react' - -const meta: Meta = { - component: H2, -} - -export default meta - -type Story = StoryObj - -export const Primary: Story = { - args: { - children: 'Heading 2', - }, - render: args =>

, -} - -export const WithIcons: Story = { - argTypes: { - icon: { - control: { - type: 'select', - options: Object.keys(iconURLs), - }, - }, - }, - args: { - children: 'Heading 2', - icon: 'trpfrog', - }, -} diff --git a/src/components/atoms/HoverScrollBox/index.module.scss b/src/components/atoms/HoverScrollBox/index.module.scss index 8217e2fa..82246230 100644 --- a/src/components/atoms/HoverScrollBox/index.module.scss +++ b/src/components/atoms/HoverScrollBox/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../styles/mixins'; .box { position: relative; diff --git a/src/components/atoms/Image.tsx b/src/components/atoms/Image.tsx new file mode 100644 index 00000000..5c8d9882 --- /dev/null +++ b/src/components/atoms/Image.tsx @@ -0,0 +1,116 @@ +'use client' + +import * as React from 'react' +import { useState } from 'react' + +import { CldImageWrapper } from '@/components/utils/CldImageWrapper' + +import { tv } from '@/lib/tailwind/variants' + +import { getPureCloudinaryPath } from '@blog/_lib/cloudinaryUtils' + +interface ImageProps + extends Omit, 'width' | 'height' | 'src'> { + src: string + width: number + height: number + withSpoiler?: boolean +} + +const createSpoilerStyles = tv({ + slots: { + blur: [ + 'tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-full', + 'tw-rounded-md tw-duration-500', + ], + button: [ + 'tw-absolute tw-px-4 tw-py-2 tw-text-black', + 'tw-cursor-pointer tw-font-bold tw-backdrop-blur', + 'tw-bg-white/50 tw-duration-200 hover:tw-bg-white/70', + ], + }, + variants: { + blur: { + true: { + blur: 'tw-backdrop-blur-2xl has-[:hover]:tw-backdrop-blur-md', + button: ` + tw-left-1/2 tw-top-1/2 -tw-translate-x-1/2 -tw-translate-y-1/2 + tw-rounded-full + `, + }, + false: { + blur: 'tw-pointer-events-none tw-block tw-backdrop-blur-none', + button: ` + tw-pointer-events-auto + tw-left-0 tw-top-0 tw-rounded-ee-md tw-rounded-ss-md + tw-px-3 tw-py-1 tw-text-sm + `, + }, + }, + }, +}) + +function ImageSpoiler() { + const [spoilerState, setSpoilerState] = useState(true) + const styles = createSpoilerStyles({ blur: spoilerState }) + return ( +
+ +
+ ) +} + +const imageStyles = tv({ + slots: { + wrapper: 'tw-relative', + image: [ + 'tw-break-inside-avoid', + 'tw-sh tw-max-w-full tw-rounded-md tw-shadow', + 'tw-bg-trpfrog-100 dark:tw-bg-trpfrog-700', + ], + }, +})() + +export function Image(props: ImageProps) { + let { className, src, alt, width, height, withSpoiler, ...rest } = props + + const minWidth = 1000 + if (width < minWidth) { + height = Math.round(minWidth * (height / width)) + width = minWidth + } + + const maxHeight = 700 + if (height > maxHeight) { + width = Math.round(maxHeight * (width / height)) + height = maxHeight + } + + let srcPath = getPureCloudinaryPath(src).split('?')[0] + if (!srcPath.startsWith('/')) srcPath = '/' + srcPath + const blurPath = `https://res.cloudinary.com/trpfrog/image/upload/w_10${srcPath}` + + const aspectRatio = `${width}/${height}` + + return ( +
+ + {withSpoiler && } +
+ ) +} diff --git a/src/components/atoms/InlineLink.tsx b/src/components/atoms/InlineLink.tsx new file mode 100644 index 00000000..e3baf603 --- /dev/null +++ b/src/components/atoms/InlineLink.tsx @@ -0,0 +1,16 @@ +import { tv } from 'tailwind-variants' + +import { A, AProps } from '@/components/wrappers/A' + +export const inlineLinkStyle = tv({ + base: [ + 'tw-inline tw-underline', + 'tw-text-[forestgreen] visited:tw-text-[olive]', + 'dark:tw-text-trpfrog-100 dark:visited:tw-text-trpfrog-400', + ], +}) + +export function InlineLink(props: AProps) { + const { className, ...rest } = props + return +} diff --git a/src/components/atoms/Button/__snapshots__/index.test.tsx.snap b/src/components/atoms/MagicButton/__snapshots__/index.test.tsx.snap similarity index 96% rename from src/components/atoms/Button/__snapshots__/index.test.tsx.snap rename to src/components/atoms/MagicButton/__snapshots__/index.test.tsx.snap index f60c88ff..433aaba9 100644 --- a/src/components/atoms/Button/__snapshots__/index.test.tsx.snap +++ b/src/components/atoms/MagicButton/__snapshots__/index.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Button a mode snapshot testing (external link) 1`] = ` class="button" data-testid="button-component" href="https://github.com" - rel="noreferrer noopener" + rel="noopener noreferrer" target="_blank" > test diff --git a/src/components/atoms/Button/index.module.scss b/src/components/atoms/MagicButton/index.module.scss similarity index 97% rename from src/components/atoms/Button/index.module.scss rename to src/components/atoms/MagicButton/index.module.scss index 53d9e2d9..d2e22e51 100644 --- a/src/components/atoms/Button/index.module.scss +++ b/src/components/atoms/MagicButton/index.module.scss @@ -1,4 +1,4 @@ -@use '@/styles/mixins'; +@use '../../../styles/mixins'; $link-button-color: var(--link-button-color); $link-button-color-bottom: var(--link-button-color-bottom); diff --git a/src/components/atoms/Button/index.stories.tsx b/src/components/atoms/MagicButton/index.stories.tsx similarity index 84% rename from src/components/atoms/Button/index.stories.tsx rename to src/components/atoms/MagicButton/index.stories.tsx index 195675a3..9b487d26 100644 --- a/src/components/atoms/Button/index.stories.tsx +++ b/src/components/atoms/MagicButton/index.stories.tsx @@ -1,16 +1,16 @@ import { action } from '@storybook/addon-actions' -import { Button } from '.' +import { MagicButton } from '.' import type { Meta, StoryObj } from '@storybook/react' -const meta: Meta = { - component: Button, +const meta: Meta = { + component: MagicButton, } export default meta -type Story = StoryObj +type Story = StoryObj export const NextLink: Story = { parameters: { diff --git a/src/components/atoms/Button/index.test.tsx b/src/components/atoms/MagicButton/index.test.tsx similarity index 74% rename from src/components/atoms/Button/index.test.tsx rename to src/components/atoms/MagicButton/index.test.tsx index 378340f4..2ef267f7 100644 --- a/src/components/atoms/Button/index.test.tsx +++ b/src/components/atoms/MagicButton/index.test.tsx @@ -1,34 +1,36 @@ import { render, screen } from '@testing-library/react' -import { Button } from '.' +import { MagicButton } from '.' describe('Button', () => { describe('div mode', () => { test('should render correctly', () => { - render() + render(test) expect(screen.getByTestId('button-component').tagName).toBe('DIV') }) test('snapshot testing', () => { - const { asFragment } = render() + const { asFragment } = render(test) expect(asFragment()).toMatchSnapshot() }) }) describe('button mode', () => { test('should render correctly', () => { - render() + render( {}}>test) expect(screen.getByTestId('button-component').tagName).toBe('BUTTON') }) test('snapshot testing', () => { - const { asFragment } = render() + const { asFragment } = render( + {}}>test, + ) expect(asFragment()).toMatchSnapshot() }) test('should call onClick', () => { const onClick = jest.fn() - render() + render(test) screen.getByTestId('button-component').click() expect(onClick).toBeCalledTimes(1) }) @@ -36,9 +38,9 @@ describe('Button', () => { test('should not call onClick when disabled', () => { const onClick = jest.fn() render( - , + , ) screen.getByTestId('button-component').click() expect(onClick).toBeCalledTimes(0) @@ -47,18 +49,18 @@ describe('Button', () => { describe('a mode', () => { test('should render correctly', () => { - render() + render(test) expect(screen.getByTestId('button-component').tagName).toBe('A') }) test('snapshot testing (internal link)', () => { - const { asFragment } = render() + const { asFragment } = render(test) expect(asFragment()).toMatchSnapshot() }) test('snapshot testing (external link)', () => { const { asFragment } = render( - , + test, ) expect(asFragment()).toMatchSnapshot() }) @@ -73,7 +75,7 @@ describe('Button', () => { test.each(internalLinks)( 'should open internal link in current tab (%s)', href => { - render() + render(test) expect(screen.getByTestId('button-component')).not.toHaveAttribute( 'target', '_blank', @@ -84,9 +86,9 @@ describe('Button', () => { 'should open internal link in new tab if there is an externalLink attr (%s)', href => { render( - , + , ) expect(screen.getByTestId('button-component')).toHaveAttribute( 'target', @@ -103,7 +105,7 @@ describe('Button', () => { test.each(externalLinks)( 'should open external link in new tab (%s)', href => { - render() + render(test) expect(screen.getByTestId('button-component')).toHaveAttribute( 'target', '_blank', @@ -114,16 +116,16 @@ describe('Button', () => { describe('disabled', () => { test('should render correctly', () => { - render() + render(test) expect(screen.getByTestId('button-component')).toHaveAttribute('disabled') }) test('should not call onClick', () => { const onClick = jest.fn() render( - , + , ) screen.getByTestId('button-component').click() expect(onClick).not.toHaveBeenCalled() @@ -131,9 +133,9 @@ describe('Button', () => { test('should not jump to /', () => { render( - , + , ) expect(screen.getByTestId('button-component')).toHaveAttribute( 'href', diff --git a/src/components/atoms/Button/index.tsx b/src/components/atoms/MagicButton/index.tsx similarity index 84% rename from src/components/atoms/Button/index.tsx rename to src/components/atoms/MagicButton/index.tsx index 6fb93835..91cbcaa1 100644 --- a/src/components/atoms/Button/index.tsx +++ b/src/components/atoms/MagicButton/index.tsx @@ -3,9 +3,9 @@ import * as React from 'react' import classNames from 'classnames' import Link from 'next/link' -import { OpenInNewTab } from '@/components/atoms/OpenInNewTab' +import { A } from '@/components/wrappers' -import { DEVELOPMENT_HOST, PRODUCTION_HOST } from '@/lib/constants' +import { isInternalLink } from '@/lib/isInternalLink' import type { SelectedRequired } from '@/lib/types' import styles from './index.module.scss' @@ -47,15 +47,7 @@ function getType

(props: P): TagType { return props.externalLink ? 'a' : 'Link' } if ('href' in props) { - const isInternalLink = [ - '/', - '#', - 'mailto:', - PRODUCTION_HOST, - DEVELOPMENT_HOST, - ].some(prefix => props.href.startsWith(prefix)) - - if (isInternalLink) { + if (isInternalLink(props.href)) { return 'Link' } else { return 'a' @@ -78,7 +70,7 @@ function Wrapper( case 'Link': return case 'a': - return + return case 'button': return