diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index df9e0d8..a1bd4bc 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,6 +1,8 @@ import type { Preview } from '@storybook/react'; +import 'devicon/devicon.min.css'; import React from 'react'; import { I18nextProvider } from 'react-i18next'; +import '../app/app.css'; import "../app/i18n/config"; import i18n from '../app/i18n/config'; diff --git a/Makefile b/Makefile index 23bd1ea..21b77ee 100644 --- a/Makefile +++ b/Makefile @@ -15,3 +15,6 @@ init: tree: @tree -da -I "node_modules|.git|.react-router" + +test: + @npx eslint \ No newline at end of file diff --git a/app/app.css b/app/app.css index 99345d8..4185f7a 100644 --- a/app/app.css +++ b/app/app.css @@ -1,10 +1,18 @@ @import "tailwindcss"; +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap'); @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } +:root { + --main-text-color: #242525; + --grey-text-color: #9D9F9F; + --accent-color: #F5F5F5; + --based-color: #242525; +} + html, body { @apply bg-white dark:bg-gray-950; @@ -12,4 +20,6 @@ body { @media (prefers-color-scheme: dark) { color-scheme: dark; } + + font-family: "Noto Sans JP"; } diff --git a/app/components/Devicon/Devicon.stories.ts b/app/components/Devicon/Devicon.stories.ts new file mode 100644 index 0000000..e7d3b29 --- /dev/null +++ b/app/components/Devicon/Devicon.stories.ts @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Devicon } from './Devicon'; + +const meta = { + title: 'Common/Devicon', + component: Devicon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + color: { control: 'color' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Icon: Story = { + args: { + icon: 'storybook', + tooltip: 'disable', + size: '100px' + }, +}; + +export const IconWithTooltip: Story = { + args: { + icon: 'storybook', + tooltip: 'enable', + size: '100px' + }, +}; diff --git a/app/components/Devicon/Devicon.tsx b/app/components/Devicon/Devicon.tsx new file mode 100644 index 0000000..8fd384d --- /dev/null +++ b/app/components/Devicon/Devicon.tsx @@ -0,0 +1,39 @@ + +import { Tooltip } from '../Tooltip/Tooltip'; + +export interface DeviconProps { + icon: string; + tooltip?: 'enable' | 'disable'; + color?: string; + size?: string; + className?: string; +} + +const abbreviation: Record = { + "aws": "amazonwebservices", +} + +export const Devicon = ({ + icon, + tooltip = 'enable', + color = "var(--based-color)", + size = "20px", + className = "", + ...props +}: DeviconProps) => { + const Icon = ( + + ); + + return ( + (tooltip === 'disable') ? + Icon : + + {Icon} + + ); +}; diff --git a/app/components/MemberCard/MemberCard.stories.ts b/app/components/MemberCard/MemberCard.stories.ts new file mode 100644 index 0000000..c893a8b --- /dev/null +++ b/app/components/MemberCard/MemberCard.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { MemberCard } from './MemberCard'; + +const meta = { + title: 'Card/MemberCard', + component: MemberCard, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + name: "Naoto Kido", + role: "代表", + description: "よわよわプログラマ", + stacks: ["kubernetes", "aws", "java", "go", "flutter"], + headerImage: "https://pbs.twimg.com/profile_banners/1846395762277826560/1737992837/1500x500", + iconImage: "https://avatars.githubusercontent.com/u/54303857", + githubName: "naoido", + }, +}; diff --git a/app/components/MemberCard/MemberCard.tsx b/app/components/MemberCard/MemberCard.tsx new file mode 100644 index 0000000..2aaf00b --- /dev/null +++ b/app/components/MemberCard/MemberCard.tsx @@ -0,0 +1,75 @@ +import { useTranslation } from 'react-i18next'; +import { Devicon } from '../Devicon/Devicon'; +import './member-card.css'; + +export interface MemberCardProps { + name: string; + role: string; + description: string, + stacks: string[]; + headerImage: string; + iconImage: string; + githubName: string; +} + +export const MemberCard = ({ + name, + role, + description, + stacks, + headerImage, + iconImage, + githubName, + ...props +}: MemberCardProps) => { + const { t } = useTranslation(); + + return ( +
+
+
+
+
+

{ name }

+

{ role }

+

{ description }

+

{ t("card.memberCard.stack") }

+
+ { + stacks.map((stack, i) => ( + + )) + } +
+ +
+
+ ); +}; diff --git a/app/components/MemberCard/member-card.css b/app/components/MemberCard/member-card.css new file mode 100644 index 0000000..911bd64 --- /dev/null +++ b/app/components/MemberCard/member-card.css @@ -0,0 +1,92 @@ +.container { + width: 435px; + background-color: var(--accent-color); +} + +.header { + height: 166px; + background-size: cover; + position: relative; + display: flex; + justify-content: center; +} + +.header::after { + content: ""; + background-color: #0000003d; + display: block; + height: 100%; + width: 100%; +} + +.icon { + position: absolute; + height: 106px; + width: 106px; + border-radius: 100%; + bottom: -53px; + background-size: cover; + box-shadow: #afafaf 0 4px 4px; +} + +.name { + text-align: center; + font-size: 30px; + font-weight: 300; + margin-top: 64px; + letter-spacing: 1px; + color: var(--main-text-color); +} + +.role { + text-align: center; + font-size: 16px; + font-weight: 300; + letter-spacing: 1px; + color: var(--main-text-color); +} + +.description { + text-align: center; + font-size: 16px; + font-weight: 300; + margin-top: 28px; + letter-spacing: 1px; + color: var(--main-text-color); +} + +.stack-title { + text-align: center; + font-size: 16px; + font-weight: 300; + margin-top: 27px; + color: var(--grey-text-color); +} + +.stack-container { + height: 50px; + width: 100%; + border-bottom: solid 1px var(--based-color); + display: flex; + align-items: center; + justify-content: center; +} + +.stack-container > * { + margin: 0 8px; +} + +.github-container { + display: flex; + height: 40px; + justify-content: center; + align-items: center; +} + +.github-username { + text-align: center; + font-size: 16px; + font-weight: 300; + margin-left: 8px; + color: var(--main-text-color); +} \ No newline at end of file diff --git a/app/components/Tooltip/Tooltip.stories.tsx b/app/components/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000..1aab0d1 --- /dev/null +++ b/app/components/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Tooltip } from './Tooltip'; + +const meta = { + title: 'Common/Tooltip', + component: Tooltip, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + backgroundColor: { control: 'color' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const BottomTooltip: Story = { + args: { + content: 'content', + location: 'bottom', + children: ( + + ) + }, +}; diff --git a/app/components/Tooltip/Tooltip.tsx b/app/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..7719f1a --- /dev/null +++ b/app/components/Tooltip/Tooltip.tsx @@ -0,0 +1,52 @@ + +import { useRef, useState, type ReactNode } from 'react'; +import './tooltip.css'; + +export interface TooltipProps { + content: string; + children: ReactNode; + location?: 'top' | 'bottom' | 'left' | 'right'; + duration?: number, + color?: string; + backgroundColor?: string; +} + +export const Tooltip = ({ + content, + children, + duration = 1000, + location = 'bottom', + color = "#ffffff", + backgroundColor = "#242525", + ...props +}: TooltipProps) => { + const [visible, setVisible] = useState(false); + const timerRef = useRef(null); + + const click = () => { + setVisible(true); + + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + timerRef.current = setTimeout(() => setVisible(false), duration); + } + + return ( +
+ { children } +
+ { content } +
+
+ ); +}; diff --git a/app/components/Tooltip/tooltip.css b/app/components/Tooltip/tooltip.css new file mode 100644 index 0000000..03c7dfd --- /dev/null +++ b/app/components/Tooltip/tooltip.css @@ -0,0 +1,84 @@ +.tooltip-container { + position: relative; + cursor: pointer; +} + +.tooltip-box { + position: absolute; + font-size: 16px; + background-color: var(--tooltip-bg); + opacity: 0; + z-index: -99999; + padding: 2px 10px; + border-radius: 0.2em; + transition: opacity ease 0.3s; +} + +.tooltip-box[data-visible="true"] { + opacity: 1; + z-index: 0; +} + +.tooltip-top { + bottom: 115%; + left: 50%; + transform: translateX(-50%); +} + +.tooltip-bottom { + top: 115%; + left: 50%; + transform: translateX(-50%); +} + +.tooltip-left { + right: 115%; + top: 50%; + transform: translateY(-50%); +} + +.tooltip-right { + left: 115%; + top: 50%; + transform: translateY(-50%); +} + +.tooltip-box::after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-style: solid; +} + +.tooltip-top::after { + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0 5px; + border-color: var(--tooltip-bg) transparent transparent transparent; +} + +.tooltip-bottom::after { + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent var(--tooltip-bg) transparent; +} + +.tooltip-left::after { + left: 100%; + top: 50%; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-color: transparent transparent transparent var(--tooltip-bg); +} + +.tooltip-right::after { + right: 100%; + top: 50%; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-color: transparent var(--tooltip-bg) transparent transparent; +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 7257185..a0df061 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -7,6 +7,7 @@ import { ScrollRestoration, } from "react-router"; +import 'devicon/devicon.min.css'; import type { Route } from "./+types/root"; import "./app.css"; import "./i18n/config"; diff --git a/eslint.config.js b/eslint.config.js index 243afff..6f2e684 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,11 +1,16 @@ +import js from "@eslint/js"; +import pluginReact from "eslint-plugin-react"; import { defineConfig } from "eslint/config"; import globals from "globals"; -import js from "@eslint/js"; import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; export default defineConfig([ + { + ignores: [ + "**/.react-router/**", + ], + }, { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] }, { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], languageOptions: { globals: globals.browser } }, { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"] }, diff --git a/package-lock.json b/package-lock.json index 26f74b0..87f6377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "dependencies": { "@react-router/node": "^7.3.0", "@react-router/serve": "^7.3.0", + "devicon": "^2.16.0", "i18next": "^24.2.3", "isbot": "^5.1.17", "react": "^19.0.0", @@ -5999,6 +6000,12 @@ "dev": true, "license": "MIT" }, + "node_modules/devicon": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/devicon/-/devicon-2.16.0.tgz", + "integrity": "sha512-PE5a2HBNeN4av+Iu975OiiWEwS8LJPw5HAvlv0JUHb62jZTdYxTpz4ga+cQyvdtb3x1side2P9Sr1mmOmUkO/g==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", diff --git a/package.json b/package.json index c1d4d28..1ebc0d2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@react-router/node": "^7.3.0", "@react-router/serve": "^7.3.0", "i18next": "^24.2.3", + "devicon": "^2.16.0", "isbot": "^5.1.17", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/vite.config.ts b/vite.config.ts index f06fe9a..b706ce8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,5 +4,9 @@ import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], + plugins: [ + tailwindcss(), + !process.env.VITEST && reactRouter(), + tsconfigPaths() + ], }); \ No newline at end of file