comment and footer

This commit is contained in:
monoid 2025-04-01 00:08:06 +09:00
parent c0bbd74a34
commit 3caca2c7d5
20 changed files with 1585 additions and 6633 deletions

View file

@ -10,9 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.17",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.17"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

301
pnpm-lock.yaml generated
View file

@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@tailwindcss/vite':
specifier: ^4.0.17
version: 4.0.17(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.2))
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
@ -17,6 +23,12 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
tailwindcss:
specifier: ^4.0.17
version: 4.0.17
devDependencies:
'@eslint/js':
specifier: ^9.17.0
@ -29,7 +41,7 @@ importers:
version: 18.3.5(@types/react@18.3.18)
'@vitejs/plugin-react-swc':
specifier: ^3.5.0
version: 3.7.2(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.1))
version: 3.7.2(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.2))
eslint:
specifier: ^9.17.0
version: 9.18.0(jiti@2.4.2)
@ -50,7 +62,7 @@ importers:
version: 8.20.0(eslint@9.18.0(jiti@2.4.2))(typescript@5.6.3)
vite:
specifier: ^6.0.5
version: 6.0.9(jiti@2.4.2)(lightningcss@1.29.1)
version: 6.0.9(jiti@2.4.2)(lightningcss@1.29.2)
packages:
@ -440,6 +452,84 @@ packages:
'@swc/types@0.1.17':
resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==}
'@tailwindcss/node@4.0.17':
resolution: {integrity: sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==}
'@tailwindcss/oxide-android-arm64@4.0.17':
resolution: {integrity: sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.0.17':
resolution: {integrity: sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.0.17':
resolution: {integrity: sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.0.17':
resolution: {integrity: sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.0.17':
resolution: {integrity: sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.0.17':
resolution: {integrity: sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.0.17':
resolution: {integrity: sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.0.17':
resolution: {integrity: sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.0.17':
resolution: {integrity: sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-win32-arm64-msvc@4.0.17':
resolution: {integrity: sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.0.17':
resolution: {integrity: sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.0.17':
resolution: {integrity: sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==}
engines: {node: '>= 10'}
'@tailwindcss/vite@4.0.17':
resolution: {integrity: sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww==}
peerDependencies:
vite: ^5.2.0 || ^6
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@ -550,6 +640,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -582,10 +676,13 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
hasBin: true
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
enhanced-resolve@5.18.1:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
esbuild@0.24.2:
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
@ -705,6 +802,9 @@ packages:
resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==}
engines: {node: '>=18'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@ -766,68 +866,68 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lightningcss-darwin-arm64@1.29.1:
resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==}
lightningcss-darwin-arm64@1.29.2:
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.29.1:
resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==}
lightningcss-darwin-x64@1.29.2:
resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.29.1:
resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==}
lightningcss-freebsd-x64@1.29.2:
resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.29.1:
resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==}
lightningcss-linux-arm-gnueabihf@1.29.2:
resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.29.1:
resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==}
lightningcss-linux-arm64-gnu@1.29.2:
resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.29.1:
resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==}
lightningcss-linux-arm64-musl@1.29.2:
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.29.1:
resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==}
lightningcss-linux-x64-gnu@1.29.2:
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.29.1:
resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==}
lightningcss-linux-x64-musl@1.29.2:
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.29.1:
resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==}
lightningcss-win32-arm64-msvc@1.29.2:
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.29.1:
resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==}
lightningcss-win32-x64-msvc@1.29.2:
resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.29.1:
resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==}
lightningcss@1.29.2:
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
engines: {node: '>= 12.0.0'}
locate-path@6.0.0:
@ -966,6 +1066,16 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
tailwindcss@4.0.17:
resolution: {integrity: sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==}
tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -1301,6 +1411,67 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
'@tailwindcss/node@4.0.17':
dependencies:
enhanced-resolve: 5.18.1
jiti: 2.4.2
tailwindcss: 4.0.17
'@tailwindcss/oxide-android-arm64@4.0.17':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.0.17':
optional: true
'@tailwindcss/oxide-darwin-x64@4.0.17':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.0.17':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.0.17':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.0.17':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.0.17':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.0.17':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.0.17':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.0.17':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.0.17':
optional: true
'@tailwindcss/oxide@4.0.17':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.0.17
'@tailwindcss/oxide-darwin-arm64': 4.0.17
'@tailwindcss/oxide-darwin-x64': 4.0.17
'@tailwindcss/oxide-freebsd-x64': 4.0.17
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.17
'@tailwindcss/oxide-linux-arm64-gnu': 4.0.17
'@tailwindcss/oxide-linux-arm64-musl': 4.0.17
'@tailwindcss/oxide-linux-x64-gnu': 4.0.17
'@tailwindcss/oxide-linux-x64-musl': 4.0.17
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.17
'@tailwindcss/oxide-win32-x64-msvc': 4.0.17
'@tailwindcss/vite@4.0.17(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@tailwindcss/node': 4.0.17
'@tailwindcss/oxide': 4.0.17
lightningcss: 1.29.2
tailwindcss: 4.0.17
vite: 6.0.9(jiti@2.4.2)(lightningcss@1.29.2)
'@types/estree@1.0.6': {}
'@types/json-schema@7.0.15': {}
@ -1393,10 +1564,10 @@ snapshots:
'@typescript-eslint/types': 8.20.0
eslint-visitor-keys: 4.2.0
'@vitejs/plugin-react-swc@3.7.2(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.1))':
'@vitejs/plugin-react-swc@3.7.2(vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@swc/core': 1.10.8
vite: 6.0.9(jiti@2.4.2)(lightningcss@1.29.1)
vite: 6.0.9(jiti@2.4.2)(lightningcss@1.29.2)
transitivePeerDependencies:
- '@swc/helpers'
@ -1441,6 +1612,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -1465,8 +1638,12 @@ snapshots:
deep-is@0.1.4: {}
detect-libc@1.0.3:
optional: true
detect-libc@2.0.3: {}
enhanced-resolve@5.18.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.2.1
esbuild@0.24.2:
optionalDependencies:
@ -1627,6 +1804,8 @@ snapshots:
globals@15.14.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
has-flag@4.0.0: {}
@ -1650,8 +1829,7 @@ snapshots:
isexe@2.0.0: {}
jiti@2.4.2:
optional: true
jiti@2.4.2: {}
js-tokens@4.0.0: {}
@ -1674,51 +1852,50 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lightningcss-darwin-arm64@1.29.1:
lightningcss-darwin-arm64@1.29.2:
optional: true
lightningcss-darwin-x64@1.29.1:
lightningcss-darwin-x64@1.29.2:
optional: true
lightningcss-freebsd-x64@1.29.1:
lightningcss-freebsd-x64@1.29.2:
optional: true
lightningcss-linux-arm-gnueabihf@1.29.1:
lightningcss-linux-arm-gnueabihf@1.29.2:
optional: true
lightningcss-linux-arm64-gnu@1.29.1:
lightningcss-linux-arm64-gnu@1.29.2:
optional: true
lightningcss-linux-arm64-musl@1.29.1:
lightningcss-linux-arm64-musl@1.29.2:
optional: true
lightningcss-linux-x64-gnu@1.29.1:
lightningcss-linux-x64-gnu@1.29.2:
optional: true
lightningcss-linux-x64-musl@1.29.1:
lightningcss-linux-x64-musl@1.29.2:
optional: true
lightningcss-win32-arm64-msvc@1.29.1:
lightningcss-win32-arm64-msvc@1.29.2:
optional: true
lightningcss-win32-x64-msvc@1.29.1:
lightningcss-win32-x64-msvc@1.29.2:
optional: true
lightningcss@1.29.1:
lightningcss@1.29.2:
dependencies:
detect-libc: 1.0.3
detect-libc: 2.0.3
optionalDependencies:
lightningcss-darwin-arm64: 1.29.1
lightningcss-darwin-x64: 1.29.1
lightningcss-freebsd-x64: 1.29.1
lightningcss-linux-arm-gnueabihf: 1.29.1
lightningcss-linux-arm64-gnu: 1.29.1
lightningcss-linux-arm64-musl: 1.29.1
lightningcss-linux-x64-gnu: 1.29.1
lightningcss-linux-x64-musl: 1.29.1
lightningcss-win32-arm64-msvc: 1.29.1
lightningcss-win32-x64-msvc: 1.29.1
optional: true
lightningcss-darwin-arm64: 1.29.2
lightningcss-darwin-x64: 1.29.2
lightningcss-freebsd-x64: 1.29.2
lightningcss-linux-arm-gnueabihf: 1.29.2
lightningcss-linux-arm64-gnu: 1.29.2
lightningcss-linux-arm64-musl: 1.29.2
lightningcss-linux-x64-gnu: 1.29.2
lightningcss-linux-x64-musl: 1.29.2
lightningcss-win32-arm64-msvc: 1.29.2
lightningcss-win32-x64-msvc: 1.29.2
locate-path@6.0.0:
dependencies:
@ -1855,6 +2032,12 @@ snapshots:
dependencies:
has-flag: 4.0.0
tailwind-merge@3.0.2: {}
tailwindcss@4.0.17: {}
tapable@2.2.1: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@ -1883,7 +2066,7 @@ snapshots:
dependencies:
punycode: 2.3.1
vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.1):
vite@6.0.9(jiti@2.4.2)(lightningcss@1.29.2):
dependencies:
esbuild: 0.24.2
postcss: 8.5.1
@ -1891,7 +2074,7 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.1
lightningcss: 1.29.2
which@2.0.2:
dependencies:

View file

@ -1,263 +1,211 @@
import './common.css'
import './minor.css'
import './content.css'
import './App.css'
import { format } from 'date-fns';
import { CommentItem, CommentListContainer, SubCommentData } from './Comment';
import CommentHeader from './CommentHeader';
import { Footer } from './Footer';
import { GalleryContent } from './Gallery';
import { GalleryTitleHeader } from './GalleryTitleHeader';
import { GalleryTable, TableRowData } from './table';
function getTimeFromNow(date: Date) {
const now = new Date()
if (now.getFullYear() !== date.getFullYear()) {
return format(date, "yyyy.MM.dd")
}
return format(date, "HH:mm")
}
const tableData: TableRowData[] = [
{ // Survey Example
id: 2995,
category: "설문",
titleText: "어떤 상황이 와도 가족 안 굶길 것 같은 생활력 강해 보이는 스타는?",
variant: "icon_survey",
author: {
type: "operator",
},
date: "25.03.24",
views: "-",
recommendations: "-",
isAdOrSurvey: true,
},
{ // AD Example
id: 2996,
category: "AD",
titleText: "트릭컬 지금 접속 시 죠안 확정권 지급",
variant: "icon_ad",
author: {
type: "operator",
},
date: "25.03.28",
views: "-",
recommendations: "-",
isAdOrSurvey: true,
},
{ // Notice Example 1
id: 1012381,
category: "공지",
titleText: "3월 5주차 주던",
commentCount: 7,
variant: "icon_notice",
author: {
type: "nickname",
nickname: "보배단",
userType: "manager", // Optional, if applicable
},
date: "25.03.27",
views: 3178,
recommendations: 12,
isNotice: true,
},
{ // Notice Example 2
id: 784571,
category: "공지",
titleText: "인방 떡밥은 별도의 갤러리를 이용해주세요",
commentCount: 3,
variant: "icon_notice",
author: {
type: "nickname",
nickname: "ㅇㅇ",
userType: "submanager", // Optional, if applicable
},
date: "25.03.08",
views: 7018,
recommendations: 6,
isNotice: true,
},
{ // Normal Row Example 1
id: 1040045,
category: "일반",
titleText: "버서커 헬 졸업해도되는데 태초한번도 안떠서 계속 돌림",
variant: "icon_pic",
commentCount: 2,
author: {
type: "nickname",
nickname: "븜구리",
userType: "manager", // Optional, if applicable
},
date: "21:58",
views: 4,
recommendations: 0,
},
{ // Normal Row Example 2 (IP Author)
id: 1040043,
category: "일반",
titleText: "혹시 엘마가 진각무기압타 빗자루 쓸 수 있음?",
variant: "icon_txt",
author: {
type: "IP",
ip: "1.248",
},
date: "21:58",
views: 1,
recommendations: 0,
},
// ... Add all other rows from the original HTML here
{ // News Row Example
id: '', // No ID
category: "뉴스",
titleText: "'2025 조치원 봄꽃축제' 개최 소식 밝혀",
variant: "icon_dctrend",
date: "18:00",
views: "",
recommendations: "",
isNews: true,
titleLinkUrl: '#', // Add actual URL
},
];
type Writer = {
type: "유동닉",
/**
* IP address without the last two octet
* e.g. "192.168"
*/
ip: string
} | {
type: "반유동"
} | {
type: "고닉",
nickname: string,
?: "주딱" | "파딱"
} | {
type: "운영자"
};
interface GalleryTableRowProps {
id: number
subject: string
title: string
replyCount: number
writer: Writer
type: "icon_notice" | "icon_pic" | "icon_txt" | "icon_survey"
date: Date
viewCount: number
recommendCount: number
fixed?: boolean
}
// Sample data - replace with your actual data source/props
const comments: SubCommentData[] = [
{
id: 1,
author: { type: 'IP', ip: '222.116' }, // Assuming author is an object with type and name
text: '너 지금 상현이햄을 ■■라고',
timestamp: '03.31 17:44:25',
showDelete: true,
},
{
id: 2,
const NicknameImagePath = {
"주딱": "/fix_managernik.gif",
"파딱": "/fix_sub_managernik.gif",
"반유동": "/nik.gif",
"default": "/fix_nik.gif"
}
function RenderWriter({ writer }: { writer: Writer }) {
const type = writer.type;
if (type === "운영자") {
return <span className="nickname in" title="운영자"><em></em></span>
}
const nickname = writer.type === "고닉" ? writer.nickname : "ㅇㅇ";
return <>
<span className="nickname in" title={nickname}><em>{nickname}</em></span>
{writer.type === "유동닉" && <span className="ip">({writer.ip})</span>}
<span className="writer_nikcon">
{writer.type === "고닉" && <img src={NicknameImagePath[writer. ?? "default"]} title={`${nickname} : 완장`}
width="12" height="11" style={{ "cursor": "pointer", marginLeft: "2px" }}
alt="갤로그로 이동합니다." />}
{writer.type === "반유동" && <img src={NicknameImagePath["반유동"]} title={`${nickname} : 닉네임`}
width="12" height="11" style={{ "cursor": "pointer", marginLeft: "2px" }}
alt="갤로그로 이동합니다." />}
</span>
</>
}
author: { type: "IP", ip: "218.144" },
text: '미국인들도 저사람 얘기할때마다 동양인이 어쩌고랑 엮는데 왤케화남ㅋㅋ',
timestamp: '03.31 18:16:45',
showDelete: true,
},
{
id: 3,
function GalleryTableRow(
{
id,
subject,
title,
replyCount,
writer,
type,
date,
viewCount,
recommendCount,
fixed = false,
}: GalleryTableRowProps
) {
const wrap = (node: React.ReactNode) => fixed ? <b>{node}</b> : node;
author: { type: "IP", ip: "183.96" },
return <tr className="ub-content us-post" data-no={id.toString()} data-type={type}>
<td className="gall_num">{id}</td>
<td className="gall_subject"><b>{subject}</b></td>
<td className="gall_tit ub-word">
<a href="/mgallery/board/view/?id=genrenovel&amp;no=9101266&amp;page=1" view-msg="">
<em className={`icon_img ${type}`}></em>
{wrap(title)}
</a>
<a className="reply_numbox">
<span className="reply_num">[{replyCount}]</span>
</a>
</td>
<td className="gall_writer ub-writer" data-nick={writer} data-ip="" data-loc="list">
{wrap(
<RenderWriter writer={writer} />
)}
</td>
<td className="gall_date" title={format(date, "yyyy-MM-dd hh:mm:ss")}>
{getTimeFromNow(date)}
</td>
<td className="gall_count">{viewCount}</td>
<td className="gall_recommend">{recommendCount}</td>
</tr>
}
text: '다른 나라랑 다르게 미국에선 ~계 미국인이라는 정체성이 확고하고 사회적인 인식도 그럼',
timestamp: '03.31 18:18:40',
showDelete: true,
},
{
id: 4,
author: { type: "semi-nickname", nickname: "ㅇㅇ" },
text: '이새낀 미국을 모르노 ㅋㅋㅋㅋ',
timestamp: '03.31 20:34:54',
showDelete: false, // No delete button in this example
},
{
id: 5,
author: { type: "semi-nickname", nickname: "ㅇㅇ" },
text: '어어 내려놔라',
timestamp: '03.31 21:16:46',
showDelete: false,
},
{
id: 6,
author: { type: "nickname", nickname: '티르칸쟈카' },
text: '원종원종아...',
timestamp: '03.31 21:23:59',
showDelete: false,
},
// Add more comment objects as needed
];
function GalleryTable() {
return <div className='gall_listwrap list'>
<table className="gall_list">
<caption> </caption>
<colgroup>
<col style={{ "width": "7%" }} />
<col style={{ "width": "51px" }} />
<col />
<col style={{ "width": "18%" }} />
<col style={{ "width": "6%" }} />
<col style={{ "width": "6%" }} />
<col style={{ "width": "6%" }} />
</colgroup>
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody className="listwrap2 ">
<GalleryTableRow
id={2973}
subject="설문"
title="입금 전,후 관리에 따라 외모 갭이 큰 스타는?"
writer={{ type: "운영자" }}
type="icon_survey"
date={new Date("2025-01-20 22:52:52")}
viewCount={0}
recommendCount={0}
replyCount={0}
/>
<GalleryTableRow
id={9101266}
subject="공지"
title="장르소설 마이너 갤러리 공지 모음"
writer={{ type: "고닉", nickname: "apocalypse", : "파딱" }}
type="icon_notice"
date={new Date("2024-08-08 23:55:43")}
viewCount={35080}
recommendCount={21}
replyCount={18}
/>
<GalleryTableRow
id={9073478}
subject="공지"
title="주딱 문의글"
writer={{ type: "유동닉", ip: "223.39" }}
type="icon_notice"
date={new Date("2024-08-04 07:24:58")}
viewCount={45439}
recommendCount={24}
replyCount={28}
/>
<GalleryTableRow
id={9056755}
subject="공지"
title="신문고"
writer={{ type: "유동닉", ip: "118.235" }}
type="icon_notice"
date={new Date("2024-08-01 06:47:52")}
viewCount={141598}
recommendCount={45}
replyCount={11}
/>
<GalleryTableRow
id={9861519}
subject="리뷰"
title="[리뷰대회]이민갔는데 딸이 생겼다"
writer={{ type: "고닉", nickname: "나이브르" }}
type="icon_pic"
date={new Date("2025-01-20 22:52:52")}
viewCount={10}
recommendCount={1}
replyCount={2}
/>
<GalleryTableRow
id={9861518}
subject="일반"
title="사펑불퇴 비처녀 음해는 그냥 글을 안읽은거 아니냐?"
writer={{ type: "유동닉", ip: "118.45" }}
type="icon_txt"
date={new Date("2025-01-20 22:52:51")}
viewCount={9}
recommendCount={0}
replyCount={0}
/>
<GalleryTableRow
id={9861517}
subject="일반"
title="역변영애 칼릭스를 진 남주로 할거였으면"
writer={{ type: "유동닉", ip: "220.89" }}
type="icon_txt"
date={new Date("2025-01-20 22:52:42")}
viewCount={5}
recommendCount={0}
replyCount={0}
/>
<GalleryTableRow
id={9861516}
subject="일반"
title="미3술가 좀 식네"
writer={{ type: "고닉", nickname: "오이탕후루" }}
type="icon_pic"
date={new Date("2025-01-20 22:52:31")}
viewCount={7}
recommendCount={0}
replyCount={1}
/>
</tbody>
</table>
</div>
}
function App() {
return (
<>
<div
style={{
width: "1160px",
margin: "20px auto 0",
}}
<div>
<main
className='w-[1160px] m-[20px_auto_0]'
>
<GalleryTitleHeader />
<div
style={{
borderTop: "2px solid #29367c",
width: "1158px"
}}
/>
<GalleryContent />
</div>
<GalleryTable />
</>
<section className='' >
<GalleryTitleHeader />
<div
className='border-custom-blue-dark border-2 w-[1158px]'
/>
<GalleryContent />
<CommentHeader />
<CommentListContainer>
<CommentItem comment={{
id: 1,
author: { type: "nickname", nickname: "동아리망했다" },
text: '너 지금 상현이햄을 ■■라고',
timestamp: '03.31 17:44:25',
showDelete: true,
}} />
<CommentItem comment={{
id: 2,
author: { type: "IP", ip: "218.144" },
text: '그냥 미국인인데 조센징들은 왜 조선계라고 못 넣어서 안달일까',
timestamp: '03.31 18:16:45',
showDelete: true,
subComments: comments,
}} />
<CommentItem comment={{
id: 3,
author: { type: "IP", ip: "123.245" },
text: 'aaa',
timestamp: '03.31 18:16:45',
showDelete: true,
}} />
</CommentListContainer>
<div style={{
width: "840px",
}}>
<GalleryTable data={tableData} />
</div>
</section>
</main>
<Footer />
</div>
)
}

32
src/Button.tsx Normal file
View file

@ -0,0 +1,32 @@
import { cn } from "./util/cn";
export function Button({ children, className, onClick }: {
children: React.ReactNode; className?: string; onClick?: () => void
}) {
return <button
onClick={onClick}
className={cn(`
bg-[#3b4890]
text-white
text-shadow-[#1d2761_0px_-1px]
cursor-pointer
align-middle
text-sm
font-apple
font-bold
w-[82px]
h-[35px]
pr-0.5
leading-[31px]
ml-[3px]
border
border-solid
border-custom-blue-dark
border-b-[3px]
rounded-xs
`, className)}
>
{children}
</button>
}

185
src/Comment.tsx Normal file
View file

@ -0,0 +1,185 @@
import { AuthorData } from "./table";
interface CommentDataBase {
id: number;
author: AuthorData;
text: string;
timestamp: string;
showDelete?: boolean;
}
export interface SubCommentData extends CommentDataBase {
parentId?: number;
}
export interface CommentData extends CommentDataBase {
subComments?: SubCommentData[]; // Optional sub-comments
}
export function CommentListContainer({
children,
}: {
children: React.ReactNode;
}) {
return <ul className="list-none pl-0 m-0 text-[13px]">{children}</ul>
}
export function CommentItem({ comment }: { comment: CommentData }) {
const renderSubComments = () => {
if (comment.subComments && comment.subComments.length > 0) {
return (
<CommentReplySection comments={comment.subComments} />
);
}
return null; // No sub-comments to render
}
return <>
<li className="bg-transparent text-[13px]">
<div className="border-t relative border-custom-gray-light px-[3px] pt-[9px] pb-[7px] flex"
>
<div style={{
width: '132px', marginRight: '33px', marginTop: '3px',
lineHeight: '13px'
}}>
<span className="relative text-[13px] cursor-pointer inline">
<span style={{
fontSize: '12px',
color: 'rgb(119, 119, 119)',
verticalAlign: 'top'
}}
className="font-normal"
>
<em
className="overflow-hidden text-ellipsis not-italic
align-tops pr-px inline-block text-nowrap max-w-[110px]"
> {comment.author.type === 'nickname' ? comment.author.nickname : 'ㅇㅇ'} </em>
</span>
{comment.author.type === 'nickname' && (
<a className="text-custom-gray-dark inline">
<img
className="inline-block align-middle cursor-pointer"
src="/fix_nik.gif" alt="icon"
/>
</a>
)}
</span>
</div>
<div style={{
width: '820px', cursor: 'pointer'
}}
className="flex-1"
>
<p style={{
lineHeight: '20px', cursor: 'pointer',
wordBreak: 'break-all',
overflowX: 'hidden', overflowY: 'hidden', width: '820px'
}}
className="overflow-hidden break-all cursor-pointer"
> {comment.text} </p>
</div>
<div className="flex items-center justify-between">
<span className="text-custom-gray-medium align-middle mt-px text-[12px] float-left">
{comment.timestamp}
</span>
{comment.showDelete && (
<div className="flex items-center justify-end ml-[6px] relative">
<button className="
bg-sp-img bg-[-268px_-200px] w-[13px] h-[13px]
align-top font-apple-dotum
" />
</div>
)}
</div>
</div>
</li>
{renderSubComments()}
</>
}
function CommentReplyItem({ comment }: { comment: SubCommentData }) {
// Function to handle potential HTML in author name
const renderAuthor = () => {
const authorMaxWidth = comment.author.type === 'IP' ? '84px' : '107px'; // Default to 107px if not specified
return (
<em
className="inline-block text-ellipsis overflow-hidden whitespace-nowrap align-top not-italic text-custom-gray-dark"
style={{ maxWidth: authorMaxWidth }} // Apply dynamic max-width
>
{comment.author.type === 'nickname' ? comment.author.nickname : 'ㅇㅇ'}
</em>
);
};
return (
<li className="bg-transparent pt-[9px] pr-3 pb-[7px] pl-3 border-t border-solid border-gray-300 first:border-t-0">
<div className="bg-transparent relative flow-root"> {/* Use flow-root to contain floats */}
{/* Author Info */}
<div className="bg-transparent float-left w-[133px] mr-[23px] mt-[3px] leading-[13px]">
<span className="bg-transparent relative text-[13px] cursor-pointer">
<span className="bg-transparent text-xs text-gray-700 align-top">
{renderAuthor()}
{comment.author.type === "IP" && ( // Conditionally render IP if it exists
<span className="bg-transparent font-tahoma text-[11px] text-custom-gray-medium inline-block align-[1px] ml-1">
({comment.author.ip})
</span>
)}
{comment.author.type === "nickname" && (
<a
className="text-custom-gray-dark inline ml-1"
> <img
className="inline-block align-middle cursor-pointer"
src={
comment.author.userType === "manager" ? "/fix_managernik.gif" :
comment.author.userType === "submanager" ? "/fix_sub_managernik.gif" :
"/fix_nik.gif"} alt="icon"
/>
</a>
)}
</span>
</span>
</div>
{/* Timestamp and Actions (float right first in source order for correct float behavior) */}
<div className="bg-transparent float-right">
<span className="bg-transparent float-left text-xs text-custom-gray-medium align-top mt-[1px] mr-2">
{comment.timestamp}
</span>
{comment.showDelete && ( // Conditionally render delete button
<div className="bg-transparent float-right relative top-[3px] right-0">
{/* Using top-[3px] might need adjustment based on exact alignment needs */}
<button
title="삭제" // Add title for accessibility
className="bg-transparent cursor-pointer align-top text-[0px] font-apple-dotum text-custom-gray-medium bg-sp-img bg-[-268px_-200px] leading-none w-[13px] h-[13px]"
>
{/* Text hidden by text-[0px] but good for screen readers */}
</button>
</div>
)}
</div>
{/* Comment Text */}
{/* Note: Original has width 820px container and 774px text. Simulating this might require careful flex/grid or removing float here */}
{/* Simplified approach: Let text flow, adjust margins if needed */}
<div className="bg-transparent overflow-hidden cursor-default"> {/* Use overflow-hidden to contain the text after floats */}
<p className="leading-5 cursor-default break-words pl-0">
{/* Removed float, width, relative, padding-left: 16px from original P. Adjust if layout breaks */}
{/* Original P had pl-16px (pl-4). Let's add it back relative to the start of this div */}
<span className="inline-block pl-4">{comment.text}</span> {/* Wrapped text in span to apply padding */}
</p>
</div>
</div>
</li>
);
}
export function CommentReplySection({ comments }: { comments: SubCommentData[] }) {
return (
<div className="bg-transparent overflow-hidden h-full text-[13px]"> {/* Assuming full height needed */}
<div className="bg-gray-50 mb-3 ml-[30px] border border-solid border-gray-300">
<ul className="bg-transparent list-none">
{comments.map((comment) => (
<CommentReplyItem key={comment.id} comment={comment} />
))}
</ul>
</div>
</div>
);
}

136
src/CommentHeader.tsx Normal file
View file

@ -0,0 +1,136 @@
import { useState } from 'react';
import { Separator } from './Separator';
function CommentHeader({
onCloseComment,
commentCount = 0,
commnetClosed = false,
}: {
onCloseComment?: (c: boolean) => void;
commentCount?: number;
commnetClosed?: boolean;
}) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [selectedSort, setSelectedSort] = useState('등록순'); // Default sort option
const sortOptions = ['등록순', '최신순', '답글순']; // Changed '답글수' to '답글순' to match list item
const handleSortSelection = (option: string) => {
setSelectedSort(option);
setIsDropdownOpen(false);
};
return (
<div className="h-[38px] leading-[38px] font-bold flex items-center justify-between bg-transparent text-[12px]">
{/* Left Section */}
<div className="flex items-center space-x-1">
<span> </span>
<em className="text-custom-red-text not-italic font-normal bg-transparent">
<span className="bg-transparent">{commentCount}</span>
</em>
<span></span>
{/* Custom Dropdown */}
<div className="relative inline-block ml-1 box-border leading-0"> {/* Adjusted margin from original margin-left: 4px */}
<div
className="relative inline-block bg-white w-[55px] h-[19px] pl-[5px] border border-custom-border-gray text-[11px]
text-custom-gray-dark leading-[18px] align-[1px] cursor-pointer font-normal font-apple-dotum"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<span className="bg-transparent">{selectedSort}</span>
<span className="sr-only"> </span> {/* Screen reader only text */}
<em className="inline-block absolute right-[5px] top-[7px] w-[9px] h-[5px] bg-sp-img bg-[-126px_-43px]"></em>
</div>
{/* Dropdown List */}
{isDropdownOpen && (
<ul className="absolute box-border left-[-1px] top-[19px] z-20 w-[62px] list-none bg-custom-dropdown-bg border border-custom-border-gray pt-[6px] px-[5px] pb-[4px]">
{sortOptions.map((option) => (
// Original had strange inline-block and font-size: 0px. Using block and sensible text size.
<li
key={option}
className="block leading-[16px] text-xs text-custom-dropdown-text font-normal font-apple-dotum cursor-pointer hover:bg-gray-200" // Added hover state
onClick={() => handleSortSelection(option)}
>
{option}
</li>
))}
</ul>
)}
{/* Hidden Select (kept for structure reference, usually not needed with custom dropdown) */}
<select className="hidden absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer font-apple-dotum text-xs align-middle">
<option></option>
<option></option>
<option></option> {/* Changed from 답글수 */}
</select>
</div>
{/* Hidden Reply Expand Button */}
<button className="hidden cursor-pointer align-middle font-apple-dotum text-xs h-[21px] ml-[71px] mt-[-1px]">
<span className="sr-only"> </span>
<em className="inline-block w-[21px] h-[21px] bg-sp-img bg-[-84px_-52px]"></em>
</button>
</div>
{/* Right Section */}
<div className="flex items-center"> {/* Use space-x for spacing */}
<a href="#" className="text-custom-gray-dark align-middle leading-[15px] font-apple text-[13px] font-bold no-underline hover:underline">
</a>
<Separator className='mx-2.5' />
<button className="cursor-pointer align-middle font-apple text-[13px] text-custom-gray-dark font-bold flex items-center"
onClick={() => onCloseComment?.(!commnetClosed)}
>
{commnetClosed ? (<>
<span className="font-apple text-[13px] text-custom-gray-dark font-bold"></span>
<em className="inline-block w-[9px] h-[5px] bg-sp-img bg-[-115px_-43px] ml-1 align-[2px]"></em>
</>) : (
<>
<span className="font-apple text-[13px] text-custom-gray-dark font-bold"></span>
<em className="inline-block w-[9px] h-[5px] bg-sp-img bg-[-84px_-52px] ml-1 align-[2px]"></em>
</>
)}
</button>
<Separator className='mx-2.5' />
<button className="cursor-pointer align-middle font-apple text-[13px] text-custom-gray-dark font-bold">
</button>
</div>
</div>
);
}
export function CommentMenuList({
onCloseComment,
commnetClosed = false,
}: {
onCloseComment?: (c: boolean) => void;
commnetClosed?: boolean;
}) {
return <div className="flex items-center"> {/* Use space-x for spacing */}
<a href="#" className="text-custom-gray-dark align-middle leading-[15px] font-apple text-[13px] font-bold no-underline hover:underline">
</a>
<Separator className='mx-2.5' />
<button className="cursor-pointer align-middle font-apple text-[13px] text-custom-gray-dark font-bold flex items-center"
onClick={() => onCloseComment?.(!commnetClosed)}
>
{commnetClosed ? (<>
<span className="font-apple text-[13px] text-custom-gray-dark font-bold"></span>
<em className="inline-block w-[9px] h-[5px] bg-sp-img bg-[-115px_-43px] ml-1 align-[2px]"></em>
</>) : (
<>
<span className="font-apple text-[13px] text-custom-gray-dark font-bold"></span>
<em className="inline-block w-[9px] h-[5px] bg-sp-img bg-[-84px_-52px] ml-1 align-[2px]"></em>
</>
)}
</button>
<Separator className='mx-2.5' />
<button className="cursor-pointer align-middle font-apple text-[13px] text-custom-gray-dark font-bold">
</button>
</div>
}
export default CommentHeader;

203
src/Footer.tsx Normal file
View file

@ -0,0 +1,203 @@
export function Footer() {
return <footer
className="bg-white pb-[50px]"
>
<div
className="w-[1158px] mt-[20px] mx-auto border-x border-t-2 border-b
border-x-[#cccccc] border-y-custom-blue-dark"
>
<div>
<div
className="overflow-hidden flex items-center pt-[21px] pb-[30px] border-b border-b-[#cccccc]
min-h-[108px]"
>
{[{
title: "게임",
items: [
"던전앤파이터",
"메이플스토리",
"아이돌마스터",
"리그 오브 레전드",
"마비노기",
"로스트아크",
"FC 온라인",
"승리의 여신 니케",
"사운드 볼텍스",
"포켓몬스터"
]
}, {
title: "연예/방송",
items: [
"남자 연예인",
"여자 연예인",
"방탄소년단",
"아이유",
"태연",
"기타 국내 드라마",
"걸스플래닛999",
"백종원의 골목식당",
"미스터 트롯",
"기타 미국드라마"
]
}, {
title: "스포츠",
items: [
"한화 이글스",
"롯데 자이언츠",
"해외야구",
"삼성 라이온즈",
"국내야구",
"해외축구",
"KIA 타이거즈",
"LG 트윈스",
"키움 히어로즈",
"SSG 랜더스"
]
}, {
title: "교육/금융/IT",
items: [
"비트코인",
"부동산",
"정치, 사회",
"컴퓨터 본체",
"테블릿PC",
"자격증",
"학점은행제",
"토익",
"주식",
"일어"
]
}, {
title: "여행/음식/생물",
items: [
"도시",
"여행-일본",
"편의점",
"치킨",
"기타음식",
"주류",
"식물",
"멍멍이",
"파충류, 양서류",
"야옹이",
]
}, {
title: "취미/생활",
items: [
"인터넷방송",
"판타지",
"토이",
"대출",
"역학",
"미스터리",
"카툰-연재",
"만화",
"향수, 화장품",
"안경",
]
}].map((section, index) => (
<dl
key={index}
className="border-l border-[#f1f1f1] pl-[20px] min-h-[108px] w-[170px] box-content"
>
<dt
style={{
marginBottom: "4px",
fontWeight: "bold",
textDecorationLine: "underline",
fontSize: "12px",
color: "#333333",
letterSpacing: "0.025em"
}}
className="text-[12px] text-custom-gray-dark"
>
{section.title}
</dt>
{section.items.map((item, idx) => (
<dd
key={idx}
style={{
paddingTop: "5px",
fontSize: "12px",
lineHeight: "100%"
}}
>
<a href="javascript:void(0);"
className="text-custom-gray-dark text-[11px]"
>{item}</a>
</dd>
))}
</dl>
))}
</div>
<div
style={{
lineHeight: "40px",
height: "38px",
paddingRight: "19px",
paddingLeft: "16px",
}}
className="text-custom-gray-dark overflow-hidden grid"
>
<span
style={{
fontSize: "11px",
}}
className="text-custom-gray-dark text-[11px] justify-self-end"
>
<a className="text-custom-gray-dark ml-[9px]">
<em className="inline-block mr-[4px] bg-sp-img bg-[-285px_-818px] w-2 h-2"
/>
</a>
<a
className="text-custom-gray-dark ml-[9px]"
href="javascript:void(0);"
onClick={() => {
window.scrollTo({ top: 0, behavior: "smooth" });
}}
>
<em
className="inline-block mr-[6px] bg-sp-img bg-[-52px_-50px] w-2 h-2"
/>
</a>
</span>
</div>
</div>
</div>
<div
className="text-[12px] text-custom-gray-dark divide-x divide-[ d7d7d7] mx-auto text-center pt-7 w-[1160px]"
>
<a className="px-2">
</a>
<a className="px-2">
</a>
<a className="px-2">
</a>
<a className="px-2">
</a>
<a className="px-2">
<b> </b>
</a>
<a className="px-2">
</a>
<a className="px-2">
</a>
</div>
<div
className="text-[12px] text-custom-gray-dark font-tahoma mt-[10px] mx-auto w-[1160px] text-center tracking-[0]"
>
Copyright 1999 - 2025 dcinside. All rights reserved.
</div>
</footer>
}

View file

@ -1,473 +1,284 @@
import { Separator } from "./Separator"
function GalleryContentHeader() {
return <header>
<div
style={{
marginTop: "16px",
marginBottom: "29px",
paddingBottom: "11px",
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: "rgb(238, 238, 238)"
}}
>
<h3
style={{
paddingRight: "2px",
paddingLeft: "2px",
marginBottom: "7px",
fontSize: "14px",
}}
>
<span> [] </span>
<span>
e글
</span>
<span> </span>
</h3>
<div
style={{
position: "relative",
fontSize: "13px",
cursor: "pointer",
paddingRight: "2px",
paddingLeft: "2px",
}}
>
<div style={{ float: "left" }}>
return (
<header>
{/* Outer container with margin, padding, and bottom border */}
<div className="mt-4 mb-[29px] pb-[11px] border-b border-solid border-gray-200">
{/* Post Title */}
<h3 className="px-0.5 mb-[7px] text-sm font-bold">
<span> [] </span>
<span>
<em> </em>
e글
</span>
<span
style={{
backgroundColor: "transparent",
fontFamily: "tahoma, sans-serif",
fontSize: "11px",
color: "rgb(153, 153, 153)"
}}
>
(110.15)
</span>
<span style={{ backgroundColor: "transparent", cursor: "default" }}>
<Separator />
2025.02.04 21:32:47
</span>
</div>
</h3>
{/* Metadata container */}
<div
style={{
float: "right",
paddingRight: "7px",
}}
className="relative text-[13px] cursor-pointer px-0.5 flex justify-between items-center"
>
<span style={{ cursor: "default" }}>
65
</span>
<span style={{ cursor: "default" }}>
<Separator />
0
</span>
<span style={{ cursor: "default" }}>
<Separator />
<a
style={{
backgroundColor: "rgb(238, 238, 238)",
color: "rgb(51, 51, 51)",
display: "inline-block",
height: "20px",
lineHeight: "20px",
padding: "0px 10px",
border: "1px solid rgb(204, 204, 204)",
borderRadius: "50px",
}}
>
13
</a>
</span>
</div>
<div style={{ clear: "both" }}></div>
</div>
</div>
</header>
}
function GalleryRecommendation() {
return <div
style={{
backgroundColor: "rgb(255, 255, 255)",
width: "auto",
height: "auto",
marginRight: "auto",
marginBottom: "36px",
marginLeft: "auto",
paddingTop: "19px",
border: "1px solid rgb(196, 196, 196)",
borderRadius: "2px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
clear: "both",
minWidth: "288px",
display: "table"
}}
>
<div
style={{
backgroundColor: "transparent",
textAlign: "center",
fontSize: "0px",
}}
>
<div
style={{
backgroundColor: "transparent",
overflow: "hidden",
width: "139px",
marginBottom: "2px",
display: "inline-block"
}}
>
<div
style={{
backgroundColor: "transparent",
float: "left",
width: "67px",
paddingTop: "10px",
paddingLeft: "11px",
textAlign: "center",
fontWeight: "bold",
color: "rgb(85, 85, 85)"
}}
>
<p
style={{
backgroundColor: "transparent",
color: "rgb(211, 25, 0)",
fontSize: "12px",
}}
>0</p>
<p style={{ backgroundColor: "transparent", lineHeight: "12px" }}>
{/* Left section: Author, IP, Timestamp */}
<div>
<span>
<img
style={{
backgroundColor: "transparent",
verticalAlign: "middle"
}}
/>
{/* author name */}
<em className="not-italic"> </em> {/* Adjusted em styling for clarity */}
</span>
<span
style={{
backgroundColor: "transparent",
color: "rgb(41, 54, 124)",
fontSize: "11px",
fontWeight: "normal"
}}
>
0
<span className="font-tahoma text-[11px] text-gray-500 ml-1">
(110.15)
</span>
</p>
</div>
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
float: "right",
width: "56px",
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
backgroundPositionX: "0px",
backgroundPositionY: "-315px",
display: "inline-block",
width: "56px",
height: "56px",
}}
></em>
</button>
</div>
<div
style={{
backgroundColor: "transparent",
overflowX: "hidden",
overflowY: "hidden",
width: "139px",
marginBottom: "2px",
display: "inline-block",
marginLeft: "10px",
}}
>
<span className="cursor-default">
<Separator />
2025.02.04 21:32:47
</span>
</div>
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
float: "left",
width: "56px",
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("https://nstatic.dcinside.com/dc/w/images/sp/sp_img.png?1112")',
backgroundRepeat: "no-repeat",
backgroundPositionX: "0px",
backgroundPositionY: "-377px",
display: "inline-block",
width: "56px",
height: "56px",
}}
/>
</button>
<div
style={{
backgroundColor: "transparent",
float: "right",
width: "64px",
paddingTop: "17px",
paddingRight: "12px",
textAlign: "center",
fontWeight: "bold",
color: "rgb(85, 85, 85)",
fontSize: "16px",
}}
>
<p style={{ backgroundColor: "transparent" }}> 0 </p>
{/* Right section: Views, Recommends, Comments */}
<div
className="pr-[7px]"
>
<span className="cursor-default">
65
</span>
<Separator />
<span className="cursor-default">
0
</span>
<Separator />
<span className="cursor-default">
{/* Comments link styled as a pill button */}
<a href="#" // Add appropriate link target
className="inline-block h-5 leading-5 px-2.5 bg-gray-200
text-gray-700 border border-gray-300 rounded-full
hover:bg-gray-300 hover:border-gray-400 text-xs">
13
</a>
</span>
</div>
</div>
</div>
</div>
<div
style={{
backgroundColor: "transparent",
clear: "both",
position: "relative",
height: "36px",
borderTopWidth: "1px",
borderTopStyle: "solid",
borderTopColor: "rgb(196, 196, 196)"
}}
>
<button
style={{
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
float: "left",
width: "95px",
height: "36px",
marginLeft: "1px",
borderRightWidth: "1px",
borderRightStyle: "solid",
borderRightColor: "rgb(196, 196, 196)",
color: "rgb(85, 85, 85)",
textShadow: "rgb(255, 255, 255) 0px 1px",
position: "relative"
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
width: "23px",
height: "23px",
backgroundPositionX: "-270px",
backgroundPositionY: "-983px",
marginRight: "3px",
marginTop: "2px",
verticalAlign: "-8px",
display: "inline-block"
}}
>
</em>
</button>
<button
style={{
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
width: "96px",
float: "left",
height: "36px",
marginLeft: "1px",
borderRightWidth: "1px",
borderRightStyle: "solid",
borderRightColor: "rgb(196, 196, 196)",
color: "rgb(85, 85, 85)",
textShadow: "rgb(255, 255, 255) 0px 1px",
position: "relative"
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
width: "17px",
height: "20px",
backgroundPositionX: "-74px",
backgroundPositionY: "-262px",
marginRight: "6px",
verticalAlign: "-4px",
display: "inline-block"
}}
>
</em>
</button>
<button
style={{
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
float: "left",
width: "95px",
height: "36px",
marginLeft: "1px",
color: "rgb(85, 85, 85)",
textShadow: "rgb(255, 255, 255) 0px 1px",
position: "relative"
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
width: "18px",
height: "20px",
backgroundPositionX: "-74px",
backgroundPositionY: "-241px",
marginRight: "6px",
verticalAlign: "-4px",
display: "inline-block"
}}
>
</em>
</button>
<div
style={{ clear: "both" }}
></div>
</div>
</div>
</header>
);
}
function GalleryNFTPublishButton() {
return <div
interface GalleryRecommendationProps {
recommendCount?: number;
fixedNickCount?: number;
downvoteCount?: number;
}
function GalleryRecommendation({
recommendCount = 0,
fixedNickCount = 0,
downvoteCount = 0
}: GalleryRecommendationProps) {
return <div
className="bg-white mx-auto mb-9 font-apple border border-[#c4c4c4] rounded-xs
pt-[19px] w-fit box-content">
<div className="flex items-center justify-center overflow-hidden pb-2">
<div className="flex justify-end overflow-hidden w-[139px] mb-0.5">
<div
style={{
width: "67px",
paddingTop: "10px",
paddingLeft: "11px",
textAlign: "center",
fontWeight: "bold",
color: "rgb(85, 85, 85)"
}}
>
<p className="text-base leading-[22px] text-[#d31900] font-bold"
>{recommendCount}</p>
<p
className="leading-3 flex items-end justify-center"
>
<span className="text-[16px]">
<img
className="align-middle inline-block"
src="/fix_nik.gif" alt="고정닉"
/>
&nbsp;
</span>
<span className="text-custom-blue-dark text-[11px] font-normal">
{fixedNickCount}
</span>
</p>
</div>
<button
className="font-apple text-[12px] cursor-pointer align-middle w-14"
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
backgroundPositionX: "0px",
backgroundPositionY: "-315px",
display: "inline-block",
width: "56px",
height: "56px",
}}
></em>
</button>
</div>
<div
style={{
width: "139px",
marginBottom: "2px",
display: "inline-block",
marginLeft: "10px",
}}
>
<button
style={{
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
float: "left",
width: "56px",
}}
className="font-apple"
>
<em
style={{
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
backgroundPositionX: "0px",
backgroundPositionY: "-377px",
display: "inline-block",
width: "56px",
height: "56px",
}}
/>
</button>
<div
style={{
float: "right",
width: "64px",
paddingTop: "17px",
paddingRight: "12px",
textAlign: "center",
fontWeight: "bold",
color: "rgb(85, 85, 85)",
fontSize: "16px",
}}
>
<p> {downvoteCount} </p>
</div>
</div>
</div>
<div
className="flex items-center justify-center overflow-hidden h-9 relative
border-t border-[rgb(196,196,196)] divide-[rgb(196,196,196)] divide-x"
>
<SilbechuButton />
<ShareButton />
<ReportButton />
</div>
</div>
}
function ReportButton() {
return <button
style={{
backgroundColor: "transparent",
position: "absolute",
left: "0px",
top: "0px",
fontSize: "0px",
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
width: "95px",
height: "36px",
marginLeft: "1px",
color: "rgb(85, 85, 85)",
textShadow: "rgb(255, 255, 255) 0px 1px",
position: "relative"
}}
>
<button
<em
style={{
backgroundColor: "rgb(59, 72, 144)",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
textShadow: "rgb(29, 39, 97) 0px -1px",
color: "rgb(255, 255, 255)",
overflow: "hidden",
width: "auto",
paddingRight: "12px",
paddingLeft: "11px",
height: "31px",
lineHeight: "29px",
marginLeft: "3px",
border: "1px solid rgb(41, 54, 124)",
borderRadius: "2px",
fontWeight: "bold"
}}
>
NFT
</button>
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "middle",
fontSize: "12px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
backgroundImage:
'url("https://nstatic.dcinside.com/dc/w/images/sp/sp_image.png")',
backgroundImage: 'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
display: "inline-block",
width: "17px",
height: "17px",
backgroundPositionX: "-1px",
backgroundPositionY: "-1px",
lineHeight: "0px",
marginLeft: "5px",
width: "18px",
height: "20px",
backgroundPosition: "-74px -241px",
marginRight: "6px",
verticalAlign: "-4px",
display: "inline-block"
}}
className="inline-block"
>
<span
style={{
backgroundColor: "transparent",
position: "absolute",
overflowX: "hidden",
overflowY: "hidden",
visibility: "hidden",
marginTop: "-1px",
marginRight: "-1px",
marginBottom: "-1px",
marginLeft: "-1px",
width: "0px",
top: "-9999px",
fontSize: "0px",
}}
>
</span>
</button>
</div>
</em>
</button>;
}
function ShareButton() {
return <button
style={{
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
width: "96px",
height: "36px",
marginLeft: "1px",
textShadow: "rgb(255, 255, 255) 0px 1px",
}}
className="align-middle relative cursor-pointer"
>
<em
style={{
backgroundPosition: "-74px -262px",
width: "17px",
height: "20px",
marginRight: "6px",
verticalAlign: "-4px",
}}
className="inline-block bg-sp-img"
>
</em>
</button>;
}
function SilbechuButton() {
return <button
style={{
backgroundColor: "rgb(249, 249, 249)",
cursor: "pointer",
verticalAlign: "middle",
width: "95px",
height: "36px",
marginLeft: "1px",
color: "rgb(85, 85, 85)",
textShadow: "rgb(255, 255, 255) 0px 1px",
position: "relative"
}}
>
<em
style={{
width: "23px",
height: "23px",
backgroundPositionX: "-270px",
backgroundPositionY: "-983px",
marginRight: "3px",
marginTop: "2px",
verticalAlign: "-8px",
display: "inline-block"
}}
className="bg-sp-img"
>
</em>
</button>;
}
export function GalleryContent() {
return <div
style={{
backgroundColor: "transparent",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, 굴림, Gulim, sans-serif',
fontSize: "13px",
color: "rgb(51, 51, 51)"
}}
return <article
className="text-custom-gray-dark text-[13px] font-apple"
>
<GalleryContentHeader />
<div style={{ lineHeight: "22px" }}>
<div style={{ backgroundColor: "transparent", marginBottom: "50px" }}>
<div style={{ marginBottom: "50px" }}>
<div
style={{
overflow: "hidden",
@ -475,10 +286,9 @@ export function GalleryContent() {
}}
>
<div>
<span style={{ backgroundColor: "transparent", marginLeft: "10px" }}>
<span style={{ marginLeft: "10px" }}>
<img
style={{
backgroundColor: "transparent",
maxWidth: "100%",
width: "550px",
height: "350px",
@ -489,8 +299,6 @@ export function GalleryContent() {
<pre />
<div
style={{
backgroundColor: "transparent",
overflow: "hidden",
width: "900px",
}}
>
@ -505,29 +313,18 @@ export function GalleryContent() {
</div>
</div>
</div>
<div style={{ backgroundColor: "transparent", position: "relative" }}>
<GalleryNFTPublishButton />
<GalleryRecommendation />
<div className="relative">
<GalleryRecommendation
recommendCount={2}
fixedNickCount={1}
/>
</div>
<div
style={{
backgroundColor: "transparent",
width: "100%",
overflow: "hidden",
textAlign: "center",
marginTop: "10px",
}}
className="w-full overflow-hidden text-center mt-2"
/>
<div
style={{
backgroundColor: "transparent",
width: "728px",
marginTop: "20px",
marginRight: "auto",
marginLeft: "auto"
}}
>
</div>
className="w-[728px] mt-[20px] mx-auto"
/>
</div>
</div>
</article>
}

View file

@ -1,183 +1,50 @@
import { Separator } from "./Separator";
import { Separator } from "./Separator"
export function GalleryTitleHeader() {
return <header>
<div
style={{
backgroundColor: "transparent",
height: "37px",
marginBottom: "3px",
paddingTop: "4px",
}}
>
<div style={{ float: "left" }}>
<h2
style={{
backgroundColor: "transparent",
marginTop: "-2px",
marginRight: "6px",
marginLeft: "2px",
float: "left",
fontSize: "24px",
maxWidth: "420px",
fontFamily: "'Nanum Gothic', sans-serif",
letterSpacing: "-1px",
margin: "2px 8px 0 3px",
"textOverflow": "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
color: "#29367c",
}}
>
<a style={{ color: "rgb(41, 54, 124)" }}>
<div
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
display: "inline-block",
verticalAlign: "top",
marginLeft: "4px",
width: "22px",
height: "22px",
backgroundPositionX: "-195px",
backgroundPositionY: "-844px",
marginTop: "3px",
}}
>
</div>
</a>
</h2>
<div style={{ clear: "both" }}></div>
</div>
<div style={{ backgroundColor: "transparent", float: "right", paddingTop: "12px" }}>
<div style={{
backgroundColor: "transparent",
position: "relative",
display: "inline-block"
}}>
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "top",
fontSize: "12px",
color: "rgb(51, 51, 51)",
position: "relative"
}}
>
</button>
<span style={{ backgroundColor: "transparent", marginRight: "2px" }}>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
display: "inline-block",
width: "6px",
height: "6px",
marginLeft: "4px",
verticalAlign: "4px",
lineHeight: "30px",
backgroundPositionX: "-70px",
backgroundPositionY: "-59px"
}}
>
</em>
</span>
return (
<header className="bg-transparent h-[37px] mb-[3px] pt-[4px] text-gray-500">
<div className="flex justify-between items-center">
<div className="flex items-center">
<h2 className="mt-[-2px] mr-[6px] ml-[2px] text-[24px] max-w-[420px] font-[nanumGothic] tracking-[-1px] m-[2px_8px_0_3px] overflow-hidden whitespace-nowrap text-ellipsis text-custom-blue-dark">
<a className="text-custom-blue-dark font-bold">
<div className="bg-sp-img bg-no-repeat inline-block align-top ml-[4px] w-[22px] h-[22px] bg-[-195px_-844px] mt-[3px]">
</div>
</a>
</h2>
</div>
<div className="flex items-center text-[12px]">
<div className="relative inline-block">
<button className="cursor-pointer align-top relative">
</button>
<span className="mr-[2px] leading-10px">
<em className="sr-only">New</em>
<em className="bg-sp-img bg-no-repeat inline-block w-[6px] h-[6px] ml-[4px] align-[4px] leading-[30px] bg-[-70px_-59px]">
</em>
</span>
</div>
<Separator />
<button className="cursor-pointer align-top font-sans">
(2/8)
<span className="mr-[2px] ml-[2px] hidden">
</span>
<em className="bg-sp-img bg-no-repeat inline-block w-[9px] h-[5px] bg-[-115px_-43px] align-[1px] ml-[2px]">
</em>
</button>
<Separator />
<button className="cursor-pointer align-top">
</button>
<Separator />
<button className="cursor-pointer align-top"></button>
<Separator />
<button className="cursor-pointer align-top">
<em className="bg-sp-img bg-no-repeat inline-block w-[15px] h-[15px] bg-[-56px_-168px] mt-[1px]">
</em>
</button>
</div>
<Separator />
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "top",
fontSize: "12px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif',
color: "rgb(51, 51, 51)"
}}
>
(2/8)
<span
style={{
backgroundColor: "transparent",
marginRight: "2px",
marginLeft: "2px",
display: "none"
}}
>
</span>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
display: "inline-block",
width: "9px",
height: "5px",
backgroundPositionX: "-115px",
backgroundPositionY: "-43px",
verticalAlign: "1px",
marginLeft: "2px",
}}
>
</em>
</button>
<Separator />
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "top",
fontSize: "12px",
color: "rgb(51, 51, 51)"
}}
>
</button>
<Separator />
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "top",
fontSize: "12px",
color: "rgb(51, 51, 51)"
}}
></button>
<Separator />
<button
style={{
backgroundColor: "transparent",
cursor: "pointer",
verticalAlign: "top",
fontSize: "12px",
color: "rgb(51, 51, 51)"
}}
>
<em
style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
display: "inline-block",
width: "15px",
height: "15px",
backgroundPositionX: "-56px",
backgroundPositionY: "-168px",
marginTop: "1px",
}}
>
</em>
</button>
</div>
</div>
</header >
}
</header>
);
}

View file

@ -1,11 +1,6 @@
export function Separator() {
return <div style={{
content: "",
display: "inline-block",
width: "1px",
height: "12px",
background: "#ccc",
margin: "0 10px 0 6px",
verticalAlign: "-1px",
}} />;
import { cn } from "./util/cn";
export function Separator({ className } : { className?: string }) {
return <div className={cn("inline-block h-3 w-[1px] bg-gray-400 mx-2 my-0 align-[-1px]",
className)} />;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
@import "tailwindcss";
/* --- Extend Tailwind Theme using @theme --- */
@theme {
--color-custom-blue-dark: rgb(41 54 124);
--color-custom-gray-dark: rgb(51 51 51);
--color-custom-gray-medium: rgb(153 153 153);
--color-custom-gray-light: rgb(238 238 238);
--color-custom-green: rgb(0 153 51);
--color-custom-red-text: rgb(211 25 0);
--color-custom-border-gray: rgb(204 204 204);
--color-custom-dropdown-bg: rgb(243 243 243);
--color-custom-dropdown-text: rgb(85 85 85);
--font-apple: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, 굴림, Gulim, sans-serif;
--font-apple-dotum: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", arial, Dotum, 돋움, sans-serif;
--font-tahoma: tahoma, sans-serif;
}
@utility bg-sp-img{
background-image: url('/sp_image.png');
background-repeat: no-repeat;
}

View file

@ -1,6 +1,6 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import "./reset.css"
import './index.css'
import App from './App.tsx'

File diff suppressed because it is too large Load diff

0
src/table.css Normal file
View file

235
src/table.tsx Normal file
View file

@ -0,0 +1,235 @@
import { cn } from './util/cn';
export type AuthorData = {
// 운영자
type: "operator"
} | {
// 고닉
// 중복 불가능 닉네임
type: "nickname",
nickname: string,
// 파딱, 주딱 구분을 위한 userType
userType?: "manager" | "submanager" // Optional, if applicable
} | {
// 유동닉
type: "IP",
// IP 주소
ip: string,
} | {
// 반유동닉
// 중복 가능 닉네임
type: "semi-nickname",
nickname: string,
}
// --- Data Interface (Remains the same) ---
export interface TableRowData {
id: string | number;
category: string;
titleText: string;
commentCount?: number;
// e.g., "icon_notice", "icon_pic", "icon_txt", "icon_survey"
variant?: "icon_notice" | "icon_recoming" | "icon_recomovie" | "icon_pic" | "icon_txt" | "icon_survey"
| "icon_ad" | "icon_dctrend";
// e.g., "운영자", "고닉", "반유동", "유동닉"
author?: {
type: "operator"
} | {
type: "nickname",
nickname: string,
userType?: "manager" | "submanager" // Optional, if applicable
} | {
type: "IP",
ip: string,
} | {
type: "semi-nickname",
nickname: string,
}
date: string;
views: "" | "-" | number;
recommendations: "" | "-" | number;
// Special flags for row types affecting style/structure
isNotice?: boolean;
isAdOrSurvey?: boolean;
isNews?: boolean; // Handle the last row type specifically if needed
titleLinkUrl?: string; // Optional URL for title
authorLinkUrl?: string; // Optional URL for author
}
// --- Child Component: TableRow ---
interface TableRowProps {
rowData: TableRowData;
}
const NicknameImagePath = {
"주딱": "/fix_managernik.gif",
"파딱": "/fix_sub_managernik.gif",
"반유동": "/nik.gif",
"default": "/fix_nik.gif"
}
export function TableRow({ rowData }: TableRowProps) {
const {
id,
category,
titleText,
commentCount,
variant,
date,
views,
recommendations,
isNotice,
isAdOrSurvey,
isNews,
titleLinkUrl = "#",
authorLinkUrl = "#",
author,
} = rowData;
const iconTable = {
icon_notice: "0px 0px",
icon_recoming: "0px -193px",
icon_recomovie: "0px -193px",
icon_pic: "0px -100px",
icon_txt: "0px -123px",
icon_survey: "0px -170px",
icon_ad: "0px -193px",
icon_dctrend: "-3px -877px", // Example for news
}
const iconPosition = iconTable[variant ?? "icon_txt"]; // Default to undefined if not found
// --- Base Cell Styles ---
// Note: some styles like height/padding might be slightly different due to Tailwind defaults vs specific px
const tdBaseClasses = 'border-t border-custom-gray-light align-middle text-custom-gray-dark h-[25px] relative px-1 py-[2px]'; // Approximates tdBaseStyle
const tdCenterClasses = cn(tdBaseClasses, "text-center font-tahoma text-[11px] pt-[1px] pb-[2px]"); // Approximates tdCenterStyle
// --- Standard Row Rendering ---
return (
<tr className="bg-transparent hover:bg-custom-gray-light">
<td className={tdCenterClasses}>{isNews ? "" : id}</td>
<td className={cn(
tdCenterClasses,
'text-xs', // Base font size for category is 12px
(isAdOrSurvey || isNotice) && 'font-bold', // Conditional bold
)}>{category}</td>
<td className={cn(tdBaseClasses, "text-left text-[13px] h-[29px]")}>
<a href={titleLinkUrl} className={cn(
'text-custom-gray-dark inline-block max-w-[82%] align-middle text-ellipsis whitespace-nowrap overflow-hidden pt-[1px] leading-tight', // Approximates titleLinkStyle
"hover:underline",
isNews && '!text-custom-green', // News specific color override
(isNotice || isAdOrSurvey) && 'font-bold', // Title bold for Notice/Ad/Survey
)}>
{iconPosition && (
<em className="inline-block w-[15px] h-[15px] align-[-3px] mr-[7px] bg-no-repeat" style={{
background: `url("/icon_img.png?1012")`,
backgroundPosition: iconPosition,
}}></em>
)}
{/* Title text - boldness handled by titleLinkClasses */}
{titleText}
</a>
{commentCount !== undefined && commentCount > 0 && (
<span className="text-xs text-custom-gray-medium ml-1 align-middle tracking-[-0.05em]">
[{commentCount}]
</span>
)}
</td>
<td className={cn(
tdCenterClasses, // Base style for author cell
'text-[13px]', // Author cell often uses 13px base font size
authorLinkUrl !== '#' ? 'cursor-pointer' : 'cursor-default', // Conditional cursor
)}>
{author?.type === "operator" ? (
<b className="font-bold"></b>
) : (
<>
{/* Author Name Span */}
<span className="inline-block max-w-[81%] align-top text-ellipsis overflow-hidden whitespace-nowrap">
{/* Inner em/span for potential finer control if needed */}
<em className="not-italic leading-[13px]">
{author?.type === "nickname" ? author?.nickname : "ㅇㅇ"}
</em>
</span>
{/* Author IP */}
{author?.type === "IP" && (
<span className="font-tahoma text-[11px] text-custom-gray-medium tracking-[-0.05em] ml-[3px]">
({author.ip})
</span>
)}
{/* Author Icon Placeholder */}
{author?.type !== "IP" && (
<a href={authorLinkUrl} className="text-custom-gray-dark ml-0.5 inline-block">
{/* Replace with actual icon component or img tag */}
<img
alt="icon"
src={
author?.type === "nickname" ? (
author?.userType === "manager" ? NicknameImagePath["주딱"] :
author?.userType === "submanager" ? NicknameImagePath["파딱"] :
NicknameImagePath["default"]
) : NicknameImagePath["반유동"]
}
className="align-middle cursor-pointer w-3 h-3" // Example size
/>
</a>
)}
</>
)}
</td>
<td className={tdCenterClasses}>{date}</td>
<td className={tdCenterClasses}>{views}</td>
<td className={tdCenterClasses}>{recommendations}</td>
</tr>
);
}
// --- Main Component: Table ---
export function GalleryTable(props: {
data: TableRowData[];
}) {
const { data } = props; // Destructure props to get data
// --- Base TH Styles ---
const thBaseClasses = 'bg-transparent h-[37px] border-t-2 border-b border-custom-blue-dark align-middle text-center text-custom-gray-dark font-appleDotum'; // Uses config colors
return (
<table className="bg-transparent border-collapse table-fixed text-xs font-apple w-full border-b border-custom-blue-dark">
{/* Screen Reader Only Caption */}
<caption className="relative w-0 text-[0px] leading-[0] -z-10">
</caption>
{/* Column Widths */}
<colgroup>
<col style={{ width: "7%" }} />
<col style={{ width: 51 }} />
<col />
<col style={{ width: "18%" }} />
<col style={{ width: "6%" }} />
<col style={{ width: "6%" }} />
<col style={{ width: "6%" }} />
</colgroup>
{/* Table Header */}
<thead className="bg-transparent">
<tr className="bg-transparent">
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
<th className={thBaseClasses}> </th>
{/* Note: Original CSS applied right border to all TH. Tailwind border utilities apply to all sides unless specified (e.g., border-l), so the above covers it. If you only wanted specific borders, you'd adjust.*/}
</tr>
</thead>
{/* Table Body */}
<tbody className="bg-transparent">
{data.map((row, index) => (
// Using id-index key for potential non-unique IDs between notices/regular posts
<TableRow key={`${row.id}-${index}`} rowData={row} />
))}
</tbody>
</table>
);
}

6
src/util/cn.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: (string | undefined | false)[]) {
return twMerge(clsx(inputs));
}

7
tailwind.config.js Normal file
View file

@ -0,0 +1,7 @@
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{js,jsx,ts,tsx}", // Adjust path as needed
]
}

View file

@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// import tailwindcss from '@tailwindcss/vite'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})