19 |
41 |
42 | {isOpen && (
43 |
44 |
45 | {locales.map((lang) => (
46 |
57 | ))}
58 |
59 |
60 | )}
61 |
62 | {/* クリック外でメニューを閉じるためのオーバーレイ */}
63 | {isOpen && (
64 |
setIsOpen(false)}
67 | aria-hidden="true"
68 | />
69 | )}
70 |
71 | );
72 | }
--------------------------------------------------------------------------------
/src/app/components/molecules/LikeButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Heart } from 'lucide-react'
4 | import { useEffect, useState } from 'react'
5 |
6 | interface LikeButtonProps {
7 | articleId: string
8 | initialLiked?: boolean
9 | }
10 |
11 | const LikeButton: React.FC
= ({
12 | articleId,
13 | initialLiked = false,
14 | }) => {
15 | const [isLiked, setIsLiked] = useState(initialLiked)
16 | const [likeCount, setLikeCount] = useState(0)
17 | const [isLoading, setIsLoading] = useState(false)
18 | const [isAnimating, setIsAnimating] = useState(false)
19 |
20 | useEffect(() => {
21 | const fetchLikeInfo = async () => {
22 | try {
23 | const userLikes = JSON.parse(
24 | localStorage.getItem('article_likes') || '{}',
25 | )
26 | const hasLiked = !!userLikes[articleId]
27 | setIsLiked(hasLiked)
28 |
29 | // APIからいいね数を取得
30 | const response = await fetch(
31 | `${process.env.NEXT_PUBLIC_API_URL}/likes?articleId=${articleId}`,
32 | )
33 |
34 | if (response.ok) {
35 | const data = await response.json()
36 | setLikeCount(data.likeCount)
37 | }
38 | } catch (error) {
39 | console.error(error)
40 | }
41 | }
42 | fetchLikeInfo()
43 | }, [articleId])
44 |
45 | const handleToggleLike = async () => {
46 | if (isLoading) return
47 |
48 | try {
49 | setIsLoading(true)
50 | setIsAnimating(true)
51 | const newLikedState = !isLiked
52 |
53 | // ローカルストレージにいいね状態を保存
54 | const userLikes = JSON.parse(
55 | localStorage.getItem('article_likes') || '{}',
56 | )
57 | if (newLikedState) {
58 | userLikes[articleId] = true
59 | } else {
60 | delete userLikes[articleId]
61 | }
62 |
63 | localStorage.setItem('article_likes', JSON.stringify(userLikes))
64 |
65 | const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/likes`, {
66 | method: 'POST',
67 | headers: { 'Content-Type': 'application/json' },
68 | body: JSON.stringify({ articleId, liked: newLikedState }),
69 | })
70 |
71 | if (response.ok) {
72 | const data = await response.json()
73 | setLikeCount(data.likeCount)
74 | setIsLiked(newLikedState)
75 | } else {
76 | throw new Error('APIリクエストが失敗しました')
77 | }
78 | } catch (error) {
79 | console.error(error)
80 |
81 | // 失敗時にローカルストレージを元に戻す
82 | const userLikes = JSON.parse(
83 | localStorage.getItem('article_likes') || '{}',
84 | )
85 | if (isLiked) {
86 | userLikes[articleId] = true
87 | } else {
88 | delete userLikes[articleId]
89 | }
90 | localStorage.setItem('article_likes', JSON.stringify(userLikes))
91 | } finally {
92 | setIsLoading(false)
93 | // アニメーション終了後に状態をリセット
94 | setTimeout(() => {
95 | setIsAnimating(false)
96 | }, 300)
97 | }
98 | }
99 |
100 | return (
101 |
132 | )
133 | }
134 |
135 | export default LikeButton
136 |
--------------------------------------------------------------------------------
/src/app/components/molecules/ProfileCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Image from 'next/image'
4 | import Link from 'next/link'
5 | import { FaGithub } from 'react-icons/fa'
6 | import { FaXTwitter } from 'react-icons/fa6'
7 | import nextConfig from '../../../../next.config.mjs'
8 | import { useI18n } from '../../../i18n/context'
9 |
10 | const BASE_PATH = nextConfig.basePath || ''
11 |
12 | const ProfileCard = () => {
13 | const { t } = useI18n()
14 |
15 | return (
16 |
17 |
18 |
26 |
27 |
28 | motoshifurugen
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {t.profileCard.description}
54 |
55 |
56 |
57 |
58 | {t.profileCard.viewProfile}
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default ProfileCard
67 |
--------------------------------------------------------------------------------
/src/app/components/molecules/Tags.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useRouter } from 'next/navigation'
4 | import React from 'react'
5 |
6 | interface TagsProps {
7 | tags: string[]
8 | }
9 |
10 | const Tags: React.FC = ({ tags }) => {
11 | const router = useRouter()
12 |
13 | const handleClickTag = (tag: string) => {
14 | router.push(`/blog?tag=${encodeURIComponent(tag)}`)
15 | }
16 |
17 | return (
18 |
19 | {tags.map((tag: string, index: number) => (
20 | handleClickTag(tag)}
28 | >
29 | {tag}
30 |
31 | ))}
32 |
33 | )
34 | }
35 |
36 | export default Tags
37 |
--------------------------------------------------------------------------------
/src/app/components/molecules/TextArrowLinkButton.tsx:
--------------------------------------------------------------------------------
1 | // ProfileLink.tsx
2 | import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4 | import React from 'react'
5 | import nextConfig from '../../../../next.config.mjs'
6 |
7 | const BASE_PATH = nextConfig.basePath || ''
8 |
9 | interface TextArrowLinkButtonProps {
10 | text: string
11 | href: string
12 | }
13 |
14 | const TextArrowLinkButton: React.FC = ({
15 | text,
16 | href,
17 | }) => {
18 | return (
19 | <>
20 |
24 | {text}
25 |
26 | {/* テキストの右につける矢印 */}
27 |
42 |
43 | >
44 | )
45 | }
46 |
47 | export default TextArrowLinkButton
48 |
--------------------------------------------------------------------------------
/src/app/components/molecules/Toc.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useEffect } from 'react'
4 | import { useInView } from 'react-intersection-observer'
5 | import tocbot from 'tocbot'
6 |
7 | const Toc: React.FC = () => {
8 | const { ref, inView } = useInView({
9 | threshold: 0,
10 | triggerOnce: false,
11 | })
12 | useEffect(() => {
13 | tocbot.init({
14 | tocSelector: `.toc`,
15 | contentSelector: '.target-toc',
16 | headingSelector: 'h2, h3, h4',
17 | headingsOffset: 100, // ヘッダーの高さに応じて調整
18 | scrollSmoothOffset: -100,
19 | })
20 |
21 | // 不要となった tocbot インスタンスを削除
22 | return () => tocbot.destroy()
23 | }, [])
24 |
25 | return (
26 | <>
27 | {/* スクロール監視用 */}
28 |
33 |
34 | 目次
35 |
36 |
37 |
38 | >
39 | )
40 | }
41 |
42 | export default Toc
43 |
--------------------------------------------------------------------------------
/src/app/components/molecules/WorkCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { ReactElement } from 'react'
3 | import nextConfig from '../../../../next.config.mjs'
4 | import Chip from '../atoms/Chip'
5 | const BASE_PATH = nextConfig.basePath || ''
6 |
7 | interface WorkCardProps {
8 | src: string
9 | alt: string
10 | title: string | ReactElement
11 | description: string
12 | tags: string[]
13 | date: string
14 | }
15 |
16 | const WorkCard: React.FC = ({
17 | src,
18 | alt,
19 | title,
20 | description,
21 | tags,
22 | date,
23 | }) => {
24 | return (
25 |
26 |
33 |
34 |
35 | {title}
36 |
37 |
{description}
38 |
39 |
40 | {tags.map((tag, index) => (
41 |
42 | {tag}
43 |
44 | ))}
45 |
46 |
47 | {date}
48 |
49 |
50 | )
51 | }
52 |
53 | export default WorkCard
54 |
--------------------------------------------------------------------------------
/src/app/components/organisms/BlogGrid.tsx:
--------------------------------------------------------------------------------
1 | import BlogCard from '@/app/components/molecules/BlogCard'
2 | import { useEffect, useState } from 'react'
3 | import { useI18n } from '@/i18n'
4 |
5 | interface BlogGridProps {
6 | blogData: any
7 | }
8 |
9 | const BlogGrid: React.FC = ({ blogData }) => {
10 | const { t } = useI18n()
11 | const [loadIndex, setLoadIndex] = useState(10)
12 | const [isEmpty, setIsEmpty] = useState(false)
13 | const [currentPost, setCurrentPost] = useState([])
14 |
15 | useEffect(() => {
16 | if (blogData.length <= 10) {
17 | setIsEmpty(true)
18 | }
19 | }, [blogData])
20 |
21 | const displayMore = () => {
22 | const newLoadIndex = loadIndex + 10
23 | setLoadIndex(newLoadIndex)
24 | if (newLoadIndex >= blogData.length) {
25 | setIsEmpty(true)
26 | }
27 | }
28 |
29 | useEffect(() => {
30 | // 日付の新しい順にソート
31 | const sortedData = [...blogData].sort((a, b) => {
32 | const dateA = new Date(a.date).getTime()
33 | const dateB = new Date(b.date).getTime()
34 | return dateB - dateA
35 | })
36 |
37 | // 現在の表示件数分のデータを取得
38 | const currentData = sortedData.slice(0, loadIndex)
39 | setCurrentPost(currentData)
40 | }, [blogData, loadIndex])
41 |
42 | return (
43 | <>
44 |
45 | {currentPost.map((post: any) => (
46 |
47 |
48 |
49 | ))}
50 |
51 |
52 | {!isEmpty && (
53 |
59 | )}
60 |
61 | >
62 | )
63 | }
64 |
65 | export default BlogGrid
66 |
--------------------------------------------------------------------------------
/src/app/components/organisms/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SubmitButton from '../atoms/SubmitButton'
3 | import InputLongText from '../molecules/InputLongText'
4 | import InputText from '../molecules/InputText'
5 | import { useI18n } from '../../../i18n/context'
6 |
7 | const Form: React.FC = () => {
8 | const { t } = useI18n()
9 |
10 | return (
11 |
36 | )
37 | }
38 |
39 | export default Form
40 |
--------------------------------------------------------------------------------
/src/app/components/organisms/MessageBoard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { announcementsData } from './MessageData';
4 | import { useI18n } from '@/i18n';
5 |
6 | const MessageBoard = () => {
7 | const { t } = useI18n();
8 |
9 | // if (announcementsData.length > 5) {
10 | // throw new Error('お知らせは5件までです。')
11 | // }
12 |
13 | return (
14 |
15 |
{t.announcements.title}
16 |
17 | {announcementsData.map((announcement, index) => (
18 | -
22 |
44 |
45 | ))}
46 |
47 |
48 | )
49 | }
50 |
51 | export default MessageBoard
52 |
--------------------------------------------------------------------------------
/src/app/components/organisms/MessageData.ts:
--------------------------------------------------------------------------------
1 | import { Locale } from '@/i18n/types';
2 |
3 | export interface AnnouncementLink {
4 | url: string;
5 | textKey: string;
6 | }
7 |
8 | export interface AnnouncementData {
9 | date: string;
10 | categoryKey: 'blogUpdate' | 'notification';
11 | titleKey: string;
12 | link?: AnnouncementLink;
13 | }
14 |
15 | export const announcementsData: AnnouncementData[] = [
16 | {
17 | date: '2025/08/20',
18 | categoryKey: 'notification',
19 | titleKey: '2025-08-20',
20 | },
21 | {
22 | date: '2025/04/22',
23 | categoryKey: 'blogUpdate',
24 | titleKey: '2025-04-22',
25 | link: {
26 | url: 'https://furugen-island.com/my_site/blog/create_my_site_4',
27 | textKey: '2025-04-22',
28 | },
29 | },
30 | {
31 | date: '2025/04/21',
32 | categoryKey: 'blogUpdate',
33 | titleKey: '2025-04-21',
34 | },
35 | {
36 | date: '2025/04/01',
37 | categoryKey: 'notification',
38 | titleKey: '2025-04-01',
39 | link: {
40 | url: 'https://furugen-island.com/my_site/game',
41 | textKey: '2025-04-01',
42 | },
43 | },
44 | {
45 | date: '2025/03/08',
46 | categoryKey: 'blogUpdate',
47 | titleKey: '2025-03-08-blog',
48 | link: {
49 | url: 'https://furugen-island.com/my_site/blog/async_await_with_forEach',
50 | textKey: '2025-03-08-blog',
51 | },
52 | },
53 | // {
54 | // date: '2025/03/08',
55 | // categoryKey: 'notification',
56 | // titleKey: '2025-03-08-like',
57 | // link: {
58 | // url: 'https://furugen-island.com/my_site/blog',
59 | // textKey: '2025-03-08-like',
60 | // },
61 | // },
62 | // {
63 | // date: '2025/02/02',
64 | // category: 'お知らせ',
65 | // title: 'ダークモードを実装しました。',
66 | // },
67 | // {
68 | // date: '2025/01/13',
69 | // category: 'ブログ更新',
70 | // title: '記事を追加しました。',
71 | // link: {
72 | // url: 'https://furugen-island.com/my_site/blog/create_my_site_2',
73 | // text: '『Reactでポートフォリオサイトを作成する 🚀(2)』',
74 | // },
75 | // },
76 | // {
77 | // date: '2025/01/01',
78 | // category: 'ブログ更新',
79 | // title: 'あけましておめでとうございます。記事を追加しました。',
80 | // link: {
81 | // url: 'https://furugen-island.com/my_site/blog/goodbye_2024_welcome_2025',
82 | // text: '『個人的な2024年の振り返りと2025年の抱負』',
83 | // },
84 | // },
85 | // {
86 | // date: '2024/11/11',
87 | // category: 'お知らせ',
88 | // title: '当サイトをリリースしました!',
89 | // },
90 | ]
91 |
--------------------------------------------------------------------------------
/src/app/components/organisms/PageFace.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface PageFaceProps {
4 | title: string
5 | subtitle: string
6 | mainMessage: React.ReactNode
7 | }
8 |
9 | const PageFace: React.FC = ({
10 | title,
11 | subtitle,
12 | mainMessage,
13 | }) => {
14 | return (
15 | <>
16 |
17 |
18 |
{title}
19 | {subtitle}
20 |
21 |
22 | {mainMessage}
23 |
24 |
25 | >
26 | )
27 | }
28 |
29 | export default PageFace
30 |
--------------------------------------------------------------------------------
/src/app/components/organisms/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 |
3 | const SearchBar: React.FC = () => {
4 | return (
5 |
6 |
11 |
21 |
22 | )
23 | }
24 |
25 | export default SearchBar
26 |
--------------------------------------------------------------------------------
/src/app/components/organisms/SkillTimeline.tsx:
--------------------------------------------------------------------------------
1 | import { useSkills } from '../../(pages)/skills/skills'
2 | import ChartHeader from '../molecules/ChartHeader'
3 | import ChartRow from '../molecules/ChartRow'
4 |
5 | const SkillTimeline: React.FC = () => {
6 | const skills = useSkills()
7 | const max = 2024.9
8 | const totalYears = max - 2019 + 1 // グラフの長さ
9 | const years = Array.from(
10 | { length: Math.ceil(totalYears) },
11 | (_, i) => 2019 + i,
12 | )
13 |
14 | return (
15 |
16 |
17 | {skills.map((skill, index) => (
18 |
19 | ))}
20 |
21 | )
22 | }
23 |
24 | export default SkillTimeline
25 |
--------------------------------------------------------------------------------
/src/app/components/organisms/ThemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react'
4 | import { FiMoon, FiSun } from 'react-icons/fi'
5 |
6 | const ThemeSwitch = () => {
7 | const [theme, setTheme] = useState('light')
8 |
9 | useEffect(() => {
10 | if (
11 | localStorage.theme === 'dark' ||
12 | (!('theme' in localStorage) &&
13 | window.matchMedia('(prefers-color-scheme: dark)').matches)
14 | ) {
15 | document.documentElement.classList.add('dark')
16 | setTheme('dark')
17 | } else {
18 | document.documentElement.classList.remove('dark')
19 | setTheme('light')
20 | }
21 | }, [])
22 |
23 | const toggleTheme = () => {
24 | if (theme === 'light') {
25 | document.documentElement.classList.add('dark')
26 | localStorage.setItem('theme', 'dark')
27 | setTheme('dark')
28 | } else {
29 | document.documentElement.classList.remove('dark')
30 | localStorage.setItem('theme', 'light')
31 | setTheme('light')
32 | }
33 | }
34 |
35 | return (
36 |
53 | )
54 | }
55 |
56 | export default ThemeSwitch
57 |
--------------------------------------------------------------------------------
/src/app/components/organisms/WorkList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useWorks } from '../../(pages)/skills/works'
3 | import WorkCard from '../molecules/WorkCard'
4 |
5 | const WorkList: React.FC = () => {
6 | const works = useWorks()
7 |
8 | return (
9 |
10 | {works.map((work, index) => (
11 |
20 | ))}
21 |
22 | )
23 | }
24 |
25 | export default WorkList
26 |
--------------------------------------------------------------------------------
/src/app/components/templates/ArticleContent.module.css:
--------------------------------------------------------------------------------
1 | /* styles/ArticleContent.css */
2 |
3 | .articleContent {
4 | margin-bottom: 2rem;
5 | font-family: "Noto Sans JP", sans-serif;
6 | font-optical-sizing: auto;
7 | white-space: pre-wrap;
8 | word-wrap: break-word;
9 | @media (min-width: 1024px) {
10 | width: calc(100% - 352px);
11 | }
12 | }
13 |
14 | /* ------------------------- */
15 | /* 見出し */
16 | /* ------------------------- */
17 | .articleContent h2 {
18 | border-bottom: 2px solid teal;
19 | margin-bottom: 1.1rem !important;
20 | margin-top: 2.3em !important;
21 | padding-bottom: 0.3em !important;
22 | white-space: pre-wrap;
23 | word-wrap: break-word;
24 | }
25 |
26 | :global(.dark) .articleContent h2 {
27 | border-bottom: 2px solid #80CBC4;
28 | }
29 |
30 | .articleContent h3 {
31 | font-weight: bold;
32 | }
33 |
34 | /* ------------------------- */
35 | /* 文字関係 */
36 | /* ------------------------- */
37 | .articleContent p {
38 | margin: 16px 0;
39 | font-size: 16px;
40 | line-height: 1.9;
41 | white-space: pre-wrap;
42 | word-wrap: break-word;
43 | }
44 |
45 | .truncate2Lines {
46 | display: -webkit-box;
47 | -webkit-line-clamp: 2;
48 | -webkit-box-orient: vertical;
49 | overflow: hidden;
50 | text-overflow: ellipsis;
51 | white-space: normal;
52 | }
53 |
54 | /* ------------------------- */
55 | /* リスト */
56 | /* ------------------------- */
57 | .articleContent ol,
58 | .articleContent ul {
59 | list-style: initial;
60 | padding-left: 1rem;
61 | white-space: normal;
62 | }
63 |
64 | .articleContent ol {
65 | list-style-type: decimal;
66 | margin-left: 20px;
67 | }
68 |
69 | .articleContent ul {
70 | list-style-type: disc;
71 | margin-left: 20px;
72 | }
73 |
74 | /* ------------------------- */
75 | /* 引用 */
76 | /* ------------------------- */
77 | .articleContent blockquote {
78 | border-left: 4px solid #ccc;
79 | padding-left: 1rem;
80 | margin: 0.5rem 0;
81 | color: #555;
82 | white-space: pre-wrap;
83 | word-wrap: break-word;
84 | }
85 |
86 | :global(.dark) .articleContent blockquote {
87 | border-left: 4px solid #80CBC4;
88 | color: #e0e0e0;
89 | }
90 |
91 | .articleContent blockquote blockquote {
92 | border-left: 4px solid #aaa;
93 | padding-left: 16px;
94 | margin-left: 0;
95 | color: #333;
96 | }
97 |
98 | :global(.dark) .articleContent blockquote blockquote {
99 | border-left: 4px solid #80CBC4;
100 | color: #e0e0e0;
101 | }
102 |
103 | /* ------------------------- */
104 | /* コードブロック */
105 | /* ------------------------- */
106 | .articleContent code {
107 | border-radius: 0.25rem;
108 | white-space: pre;
109 | word-wrap: break-word;
110 | }
111 |
112 | /* ------------------------- */
113 | /* 表 */
114 | /* ------------------------- */
115 | .articleContent table {
116 | width: 100%;
117 | border-collapse: collapse;
118 | margin: 1rem 0;
119 | white-space: pre-wrap;
120 | word-wrap: break-word;
121 | }
122 |
123 | .articleContent th,
124 | .articleContent td {
125 | border: 1px solid #ddd;
126 | padding: 0.5rem;
127 | text-align: left;
128 | }
129 |
130 | .articleContent th {
131 | background-color: #f3f4f6;
132 | color: #4b5563;
133 | }
134 |
135 | .articleContent td {
136 | background-color: white;
137 | color: #4b5563;
138 | }
139 |
--------------------------------------------------------------------------------
/src/app/components/templates/ArticleContent.tsx:
--------------------------------------------------------------------------------
1 | import { MDXRemote } from 'next-mdx-remote/rsc'
2 | import rehypeKatex from 'rehype-katex'
3 | import rehypePrism from 'rehype-prism'
4 | import rehypeSlug from 'rehype-slug'
5 | import remarkGfm from 'remark-gfm'
6 | import remarkMath from 'remark-math'
7 |
8 | import Highlight from '@/app/components/atoms/Highlight'
9 | import CodeBlock from '@/app/components/molecules/CodeBlock'
10 | import LikeButton from '@/app/components/molecules/LikeButton'
11 | import Tags from '@/app/components/molecules/Tags'
12 | import Sidebar from '@/app/components/templates/Sidebar'
13 |
14 | import EmbedArticle from '@/app/components/molecules/EmbedArticle'
15 | import 'prismjs/components/prism-python.js'
16 | import 'prismjs/themes/prism-tomorrow.css'
17 | import React, { ReactNode } from 'react'
18 |
19 | import styles from './ArticleContent.module.css'
20 |
21 | interface ArticleContentProps {
22 | blogArticle: any
23 | SidebarComponents: React.ReactNode[]
24 | }
25 |
26 | const codeBlockComponents = {
27 | code: (
28 | props: JSX.IntrinsicAttributes & {
29 | className?: string
30 | children?: ReactNode
31 | },
32 | ) => {
33 | // インラインコードの場合(preタグでラップされていない場合)
34 | if (!props.className) {
35 | return (
36 |
37 | {String(props.children)}
38 |
39 | )
40 | }
41 |
42 | // コードブロックの場合
43 | const content = String(props.children || '')
44 | const [lang, file] = (props.className || '')
45 | .replace('language-', '')
46 | .split(':')
47 |
48 | return (
49 |
50 | {content}
51 |
52 | )
53 | },
54 | pre: (props: JSX.IntrinsicAttributes & { children?: ReactNode }) => {
55 | return
56 | },
57 | p: (props: JSX.IntrinsicAttributes & { children?: ReactNode }) => (
58 |
59 | ),
60 | a: (
61 | props: JSX.IntrinsicAttributes & { href?: string; children?: ReactNode },
62 | ) => {
63 | const { href, children } = props
64 | if (href && href.startsWith('http')) {
65 | return
66 | }
67 | return {children}
68 | },
69 | }
70 |
71 | const ArticleContent: React.FC = ({
72 | blogArticle,
73 | SidebarComponents,
74 | }) => {
75 | return (
76 |
77 |
80 |
81 |
82 | {blogArticle.date}
83 |
84 |
85 |
86 |
87 | {blogArticle.title}
88 |
89 | {blogArticle.tags &&
}
90 |
96 | {/* 目次表示に必要 */}
97 |
98 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 |
117 | export default ArticleContent
118 |
--------------------------------------------------------------------------------
/src/app/components/templates/ClientWrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import LoadingCircle from '@/app/components/atoms/LoadingCircle'
4 | import HtmlLangUpdater from '@/app/components/atoms/HtmlLangUpdater'
5 | import BackgroundWrapper from '@/app/components/molecules/BackgroundWrapper'
6 | import Footer from '@/app/components/templates/Footer'
7 | import Header from '@/app/components/templates/Header'
8 | import { I18nProvider } from '@/i18n'
9 | import React, { useEffect, useState } from 'react'
10 |
11 | const ClientWrapper: React.FC<{ children: React.ReactNode }> = ({
12 | children,
13 | }) => {
14 | const [isLoading, setIsLoading] = useState(true)
15 |
16 | useEffect(() => {
17 | setTimeout(() => {
18 | setIsLoading(false)
19 | }, 2000) // 2秒後にローディングを終了
20 | }, [])
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | export default ClientWrapper
36 |
--------------------------------------------------------------------------------
/src/app/components/templates/Footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { faPlane } from '@fortawesome/free-solid-svg-icons'
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5 | import Link from 'next/link'
6 | import { FaGithub } from 'react-icons/fa'
7 | import { FaXTwitter } from 'react-icons/fa6'
8 | import AnimatedLine from '../atoms/AnimatedLine'
9 | import { useI18n } from '@/i18n'
10 |
11 | export default function Footer() {
12 | const { t } = useI18n()
13 |
14 | return (
15 | <>
16 |
17 |
18 |
80 |
81 | >
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/components/templates/MaintenanceTemplate.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import React from 'react'
3 | import AnimatedLine from '../atoms/AnimatedLine'
4 | import PageFace from '../organisms/PageFace'
5 |
6 | interface MaintenanceTemplateProps {
7 | title: string
8 | imagePath: string
9 | }
10 |
11 | const MaintenanceTemplate: React.FC = ({
12 | title,
13 | imagePath,
14 | }) => {
15 | return (
16 | <>
17 |
20 |
21 |
22 |
23 |
34 | >
35 | )
36 | }
37 |
38 | export default MaintenanceTemplate
39 |
--------------------------------------------------------------------------------
/src/app/components/templates/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ProfileCard from '../molecules/ProfileCard'
3 |
4 | export interface SidebarTypes {
5 | SidebarComponents?: React.ReactNode[]
6 | }
7 |
8 | const Sidebar: React.FC = ({ SidebarComponents }) => {
9 | return (
10 |
16 | )
17 | }
18 |
19 | export default Sidebar
20 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 | @import "tailwindcss/components";
3 | @import "tailwindcss/utilities";
4 |
5 | @layer base {
6 | /* フォントの設定 */
7 | .noto-sans-jp {
8 | font-family: "Noto Sans JP", sans-serif;
9 | font-optical-sizing: auto;
10 | font-style: normal;
11 | @apply text-main-black;
12 | }
13 | .dm-sans {
14 | font-family: "DM Sans", sans-serif;
15 | font-optical-sizing: auto;
16 | font-style: normal;
17 | @apply text-main-black;
18 | }
19 |
20 | body {
21 | @apply bg-main-white;
22 | .dark & {
23 | @apply bg-night-black text-night-white;
24 | }
25 |
26 | main {
27 | @apply pt-28;
28 | }
29 | section {
30 | @apply container mx-auto p-4;
31 | h2 {
32 | @apply my-8;
33 | }
34 | }
35 |
36 | h1 {
37 | @apply text-2xl md:text-3xl noto-sans-jp font-bold;
38 | }
39 | h2 {
40 | @apply text-xl md:text-2xl noto-sans-jp font-bold;
41 | }
42 | h3 {
43 | @apply text-lg md:text-xl leading-loose noto-sans-jp font-semibold;
44 | }
45 | p {
46 | @apply text-sm md:text-base leading-snug md:leading-loose noto-sans-jp;
47 | }
48 | .dark & h1, .dark & h2, .dark & h3, .dark & p {
49 | @apply text-night-white;
50 | }
51 |
52 | /* ------------------------- */
53 | /* 目次 */
54 | /* ------------------------- */
55 | .toc-list {
56 | @apply w-full my-0 px-0;
57 | }
58 | .toc-link {
59 | @apply noto-sans-jp text-main-black dark:text-night-white text-sm font-normal no-underline transition-colors duration-200 rounded-md inline-block w-full py-1 pl-2;
60 | }
61 | .is-collapsible .toc-link {
62 | @apply ml-2 font-light text-xs;
63 | }
64 | .toc-list-item {
65 | @apply list-none w-full rounded-md py-0 my-0;
66 | }
67 | .toc-link::before {
68 | @apply content-[''] w-2 h-2 bg-teal dark:bg-night-teal inline-block mr-2 rounded-full;
69 | }
70 | .is-active-link {
71 | @apply bg-main-white dark:bg-night-black font-bold text-main-black dark:text-night-white;
72 | }
73 | .is-active-link::before {
74 | @apply content-[''] w-3 h-3 bg-teal dark:bg-night-teal inline-block mr-2 rounded-full;
75 | }
76 | .is-collapsible .toc-link::before {
77 | @apply w-0 h-0;
78 | }
79 | .is-collapsible .toc-link::before {
80 | @apply w-0 h-0;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/hooks/useLikeCount.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLikeCount = (articleId: string) => {
4 | const [likeCount, setLikeCount] = useState(0)
5 |
6 | useEffect(() => {
7 | const fetchLikeCount = async () => {
8 | try {
9 | const response = await fetch(
10 | `${process.env.NEXT_PUBLIC_API_URL}/likes?articleId=${articleId}`,
11 | )
12 | if (response.ok) {
13 | const data = await response.json()
14 | setLikeCount(data.likeCount)
15 | }
16 | } catch (error) {
17 | console.error('いいね数の取得に失敗しました:', error)
18 | }
19 | }
20 |
21 | fetchLikeCount()
22 | }, [articleId])
23 |
24 | return likeCount
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import ClientWrapper from '@/app/components/templates/ClientWrapper'
2 | import '@fortawesome/fontawesome-svg-core/styles.css'
3 | import { GoogleAnalytics, GoogleTagManager } from '@next/third-parties/google'
4 | import type { Metadata } from 'next'
5 | import { ThemeProvider } from 'next-themes'
6 | import { Inter } from 'next/font/google'
7 | import 'tailwindcss/tailwind.css'
8 | import './globals.css'
9 |
10 | const inter = Inter({ subsets: ['latin'] })
11 |
12 | export const metadata: Metadata = {
13 | title:
14 | "Furugen's Island | 古堅基史(なんくるないさ系エンジニア)の開発ポートフォリオサイト",
15 | description:
16 | '古堅基史(ふるげんもとし)のポートフォリオサイト。Furugen Motoshi, フロントエンドエンジニアの開発実績やブログを掲載。情熱と遊び心を持って日々挑戦しています。',
17 | icons: [
18 | {
19 | rel: 'icon',
20 | type: 'image/png',
21 | sizes: '16x16',
22 | url: '/my_site/favicon/icon-16x16.png',
23 | },
24 | {
25 | rel: 'icon',
26 | type: 'image/png',
27 | sizes: '32x32',
28 | url: '/my_site/favicon/icon-32x32.png',
29 | },
30 | {
31 | rel: 'apple-touch-icon',
32 | url: '/my_site/favicon/apple-icon.png',
33 | },
34 | {
35 | rel: 'shortcut icon',
36 | url: '/my_site/favicon/favicon.ico',
37 | },
38 | {
39 | rel: 'manifest',
40 | url: '/my_site/favicon/site.webmanifest',
41 | },
42 | ],
43 | }
44 |
45 | export default function RootLayout({
46 | children,
47 | }: Readonly<{
48 | children: React.ReactNode
49 | }>) {
50 | return (
51 |
52 |
53 | {/* Google Fonts読み込み */}
54 |
55 |
60 |
64 |
65 |
66 |
67 | {children}
68 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Metadata } from 'next'
4 | import nextConfig from '../../next.config.mjs'
5 | import MaintenanceTemplate from './components/templates/MaintenanceTemplate'
6 | const BASE_PATH = nextConfig.basePath || ''
7 |
8 | export const metadata: Metadata = {
9 | title: 'notfound - ページが見つかりません',
10 | }
11 |
12 | export default function NotFound() {
13 | return (
14 | <>
15 |
19 | >
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import MessageBoard from '@/app/components/organisms/MessageBoard'
4 | import { useI18n } from '@/i18n'
5 | import Image from 'next/image'
6 | import nextConfig from '../../next.config.mjs'
7 | import MainMessage from './components/atoms/MainMessage'
8 | import TitleAnimation from './components/atoms/TitleAnimation'
9 | import TextArrowLinkButton from './components/molecules/TextArrowLinkButton'
10 | const BASE_PATH = nextConfig.basePath || ''
11 |
12 | export default function Home() {
13 | const { t } = useI18n()
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { getAllPosts } from '@/app/api/utils/getPostData'
2 | import { MetadataRoute } from 'next'
3 |
4 | export default async function sitemap(): Promise {
5 | const baseUrl = 'https://furugen-island.com/my_site'
6 | const currentDate = new Date().toISOString()
7 |
8 | const defaultPages: MetadataRoute.Sitemap = [
9 | {
10 | url: `${baseUrl}/`,
11 | lastModified: currentDate,
12 | },
13 | {
14 | url: `${baseUrl}/profile`,
15 | lastModified: currentDate,
16 | },
17 | {
18 | url: `${baseUrl}/blog`,
19 | lastModified: currentDate,
20 | },
21 | {
22 | url: `${baseUrl}/skills`,
23 | lastModified: currentDate,
24 | },
25 | {
26 | url: `${baseUrl}/contact`,
27 | lastModified: currentDate,
28 | },
29 | ]
30 |
31 | const posts = await getAllPosts()
32 |
33 | const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
34 | url: `${baseUrl}/blog/${post.slug}`,
35 | lastModified: post.date ? new Date(post.date).toISOString() : currentDate,
36 | }))
37 |
38 | return [...defaultPages, ...blogPages]
39 | }
40 |
--------------------------------------------------------------------------------
/src/i18n/config.ts:
--------------------------------------------------------------------------------
1 | import { Locale } from './types';
2 |
3 | export const defaultLocale: Locale = 'ja';
4 | export const locales: Locale[] = ['ja', 'en'];
5 |
6 | export const localeNames: Record = {
7 | ja: '日本語',
8 | en: 'English',
9 | };
10 |
11 | // URLパスから言語を判定するヘルパー関数
12 | export function getLocaleFromPath(pathname: string): Locale {
13 | const segments = pathname.split('/').filter(Boolean);
14 | const firstSegment = segments[0];
15 |
16 | if (locales.includes(firstSegment as Locale)) {
17 | return firstSegment as Locale;
18 | }
19 |
20 | return defaultLocale;
21 | }
22 |
23 | // 言語付きのパスを生成するヘルパー関数
24 | export function getLocalizedPath(pathname: string, locale: Locale): string {
25 | const segments = pathname.split('/').filter(Boolean);
26 | const firstSegment = segments[0];
27 |
28 | // 既に言語プレフィックスがある場合は置き換え
29 | if (locales.includes(firstSegment as Locale)) {
30 | segments[0] = locale;
31 | } else {
32 | // 言語プレフィックスがない場合は追加
33 | segments.unshift(locale);
34 | }
35 |
36 | return '/' + segments.join('/');
37 | }
--------------------------------------------------------------------------------
/src/i18n/context.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
4 | import { Locale, TranslationKeys } from './types';
5 | import { translations } from './translations';
6 | import { defaultLocale, getLocaleFromPath } from './config';
7 |
8 | interface I18nContextType {
9 | locale: Locale;
10 | setLocale: (locale: Locale) => void;
11 | t: TranslationKeys;
12 | }
13 |
14 | const I18nContext = createContext(undefined);
15 |
16 | interface I18nProviderProps {
17 | children: ReactNode;
18 | initialLocale?: Locale;
19 | }
20 |
21 | export function I18nProvider({ children, initialLocale }: I18nProviderProps) {
22 | const [locale, setLocaleState] = useState(initialLocale || defaultLocale);
23 |
24 | // ローカルストレージから言語設定を読み込み
25 | useEffect(() => {
26 | if (typeof window !== 'undefined') {
27 | const savedLocale = localStorage.getItem('locale') as Locale;
28 | if (savedLocale && (savedLocale === 'ja' || savedLocale === 'en')) {
29 | setLocaleState(savedLocale);
30 | } else if (!initialLocale) {
31 | // URLパスから言語を判定
32 | const pathLocale = getLocaleFromPath(window.location.pathname);
33 | setLocaleState(pathLocale);
34 | }
35 | }
36 | }, [initialLocale]);
37 |
38 | const setLocale = (newLocale: Locale) => {
39 | setLocaleState(newLocale);
40 | if (typeof window !== 'undefined') {
41 | localStorage.setItem('locale', newLocale);
42 | }
43 | };
44 |
45 | const value: I18nContextType = {
46 | locale,
47 | setLocale,
48 | t: translations[locale],
49 | };
50 |
51 | return {children};
52 | }
53 |
54 | export function useI18n(): I18nContextType {
55 | const context = useContext(I18nContext);
56 | if (context === undefined) {
57 | throw new Error('useI18n must be used within an I18nProvider');
58 | }
59 | return context;
60 | }
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './config';
3 | export * from './translations';
4 | export * from './context';
--------------------------------------------------------------------------------
/src/posts/cakephp_install_for_mac_with_mamp.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'CakePHP3.9をMacにインストールする(MAMP使用)'
3 | date: '2020-10-02'
4 | tags:
5 | - 'PHP'
6 | - 'MAMP'
7 | - '環境構築'
8 | ---
9 |
10 | ## 環境
11 |
12 | バージョンは下記
13 |
14 | - OS:MacOS Catalina 10.15.4
15 |
16 | - PHP:7.3.22
17 |
18 | - Composer:1.10.13
19 |
20 | 今回はデータベースについては触れません
21 |
22 | 以下記事でデータベース入れました▼
23 | [CakePHP3.9にMySQLを接続する(Homebrew使用)](https://qiita.com/motoshi_cocoa/items/0e2489bfe4ae9cf9d5a9)
24 |
25 | ## MAMPのダウンロード
26 |
27 | - MAMP参考
28 | https://qiita.com/kuro-wassan/items/1cb32995acc07a4b4cc6
29 |
30 | - MAMPのサイト
31 | https://www.mamp.info/en/downloads/ からMac用のMAMPをダウンロードする。
32 |
33 | - ApplicationsフォルダのなかにMAMPがダウンロードされる。
34 |
35 | ## CakePHPのアプリの作成
36 |
37 | - CakePHP3.9 公式
38 | https://book.cakephp.org/3/ja/installation.html
39 |
40 | - MAMPが落ちたところへ行く
41 |
42 | ```bash:ターミナル
43 | $ cd Applications
44 | $ ls
45 | $ cd MAMP
46 | ```
47 |
48 | ↑MAMPが複数ある場合は名前が違うことがあるので注意
49 |
50 | - htdocsに入る(この中にCakePHPアプリをおきたい)
51 |
52 | ```bash:ターミナル
53 | $ cd htdocs
54 | ```
55 |
56 | - CakePHPアプリの作成
57 |
58 | ```bash:ターミナル
59 | $ php composer.phar create-project --prefer-dist cakephp/app:^3.8 app_name
60 | ```
61 |
62 | または
63 |
64 | ```bash:ターミナル
65 | $ composer self-update && composer create-project --prefer-dist cakephp/app:^3.8 app_name
66 | ```
67 |
68 | でCakePHPのアプリが作られる(`app_name`のところには任意のアプリ名を入れる)
69 |
70 | ```bash:ターミナル
71 | Updating to version 1.10.13 (stable channel).
72 | Downloading (100%)
73 | Use composer self-update --rollback to return to version 1.10.5
74 | Creating a "cakephp/app:^3.8" project at "./app_name"
75 | Installing cakephp/app (3.9.0)
76 | .
77 | .
78 | . 省略
79 | .
80 | .
81 | > App\Console\Installer::postInstall
82 | Created `config/app_local.php` file
83 | Created `/Applications/MAMP/htdocs/app_name/tmp/cache/views` directory
84 | Set Folder Permissions ? (Default to Y) [Y,n]?
85 | ```
86 |
87 | ↑ファイルの権限を設定していいかcomposerに聞かれるけどYと答えてEnter。
88 |
89 | - アプリが作成されているか確認してアプリフォルダに入る
90 |
91 | ```bash:ターミナル
92 | $ ls
93 | app_name
94 | $ cd app_name
95 | ```
96 |
97 | - 起動!
98 |
99 | ```bash:ターミナル
100 | $ bin/cake server
101 | ```
102 |
103 | ```bash:ターミナル
104 | Welcome to CakePHP v3.9.2 Console
105 | ---------------------------------------------------------------
106 | App : src
107 | Path: /Applications/MAMP/htdocs/app_name/src/
108 | DocumentRoot: /Applications/MAMP/htdocs/app_name/webroot
109 | Ini Path:
110 | ---------------------------------------------------------------
111 | built-in server is running in http://localhost:8765/
112 | You can exit with `CTRL-C`
113 | ```
114 |
115 | - 上記のような表示が出たら成功
116 |
117 | - ブラウザで `http://localhost:8765/` にアクセスしてみる
118 |
119 | 
120 |
121 | ここまで。
122 |
--------------------------------------------------------------------------------
/src/posts/goodbye_2024_welcome_2025.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '個人的な2024年の振り返りと2025年の抱負'
3 | date: '2024-12-31'
4 | tags:
5 | - 'ブログ'
6 | ---
7 |
8 | あっという間に2024年が終わります。今年もたくさんのことがありましたが、振り返りとしてまとめてみたいと思います。
9 | また、2025年に向けての抱負も合わせて記載します。
10 |
11 | ## 2024年の振り返り
12 |
13 | ### 仕事
14 |
15 | フロントエンド開発のプロジェクトへ移動し、最初はキャッチアップが大変でしたが、毎日が勉強で楽しみながら開発できました。
16 | Vuexによるデータ操作やVueライフサイクルの理解が深まり、少しずつですが自信を持ってコードを書けるようになってきました。🙌
17 |
18 | とはいえ、まだまだ把握できていないコードや実装方法がたくさんあるなーと感じています(バグ対応が大変だった記憶)。
19 | 一緒に働くメンバーや環境にも恵まれて裁量権を持って業務に取り組むことができているので、この環境を大切にしながらもっと技術力を高めていきたいです。
20 |
21 | 社内ハッカソンの運営も担当し、初めての経験でしたが、メンバーと協力して楽しくチーム開発を行うことができました。
22 | ▼ 詳しくはZennの企業ブログに記事を書いています。
23 | https://zenn.dev/arm_techblog/articles/569d21b0bbac67
24 |
25 |
26 | 仕事の進め方については、自身で立てた計画から遅れることが多く、バッファを持つことが課題だと感じました。
27 | また、なんくるないさ精神で行き当たりばったりで手を動かすことが多いので、もっと仮説思考を大切にして、無駄な時間を労力を減らしていきたいです。💪
28 |
29 | 基本フルリモート勤務ということもあり、自宅のデスク周り整理や生活リズムの固定も意識して取り組み、自分にとっては休憩中の散歩と換気が良いリフレッシュ方法だということも気づきました。
30 |
31 | ### 個人開発
32 |
33 | Next.jsを用いて本サイトを作成することができました。初めてのReactでしたが、同志の強いサポートもあり、とても楽しかったです!
34 | tsxの書き方やtailwindcssの使い方、Vercelへのデプロイなど、新しい技術を取り入れることができ、個人開発の幅が広がりました。
35 |
36 | まだまだ改善点は多いですが、2025年も引き続き更新していきます。✨
37 |
38 | ### 趣味
39 |
40 | 読書と短歌と散歩がマイブームでした。
41 |
42 | 今年は18冊の本に出会い、そのなかでの個人的ベストは『意識はいつ生まれるのか』です。
43 |
44 | https://amzn.asia/d/85QrPvY
45 |
46 | 短歌では、目標としていた1年で100個の短歌を作ることを達成しました。改めて言葉の面白さと難しさを感じました。
47 |
48 |
53 |
54 | 言葉といえば、2024年の自分に一番刺さった言葉は以下です。
55 |
56 | > 何を言うかが知性 何を言わないかが品性(スピードワゴン小沢)
57 |
58 | ## 2025年の抱負
59 |
60 | ### 海外旅行に行く!! 🌏🌎🌍
61 |
62 | 開発技術の公式ドキュメントやカンファレンスで発信される最新情報は英語のことが多いと改めて感じ、英語の勉強を始めました。
63 | モチベーションを高めるためと、ずっと留まっている日本から一度出てみたいという思いもあり、2025年は海外旅行に行くことを目標にします。
64 |
65 | 2025年もよろしくお願いします。
--------------------------------------------------------------------------------
/src/posts/message_to_my_colleagues.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'ARM 23卒 同期へ'
3 | date: '2025-06-01'
4 | tags:
5 | - '限定公開'
6 | - 'noindex'
7 | ---
8 |
9 | **【ご報告】**
10 | **勝手ですが、7月末をもってARMを退社し、海外に行く準備をします。**
11 |
12 | ## このページの要約(ChatGPT)
13 |
14 | > 2年間お世話になりました!
15 | > エンジニアとしての自信を得て、海外挑戦を決意しました。
16 | > まずはフィリピンで英語を猛特訓し、その後はカナダへ向かいます。
17 | > 戻る予定はありませんが、みんなのことは絶対に忘れません。
18 | > 残りわずかですが、最後までよろしくお願いします!🙇♂️
19 |
20 |
26 |
27 | ※語りたがり発動につき、以下長文注意。
28 | お忙しい方はそっとタブを閉じてください。
29 |
30 | ## 入社から2年、感謝の気持ちを込めて
31 |
32 | みんな、本当にお世話になりました。
33 | 2023年4月3日、眉間に絆創膏を貼って入社初日を迎えたあの日から、気づけばもう2年。
34 | 妙蓮寺↔︎中目黒間はいまだに長く感じる一方で、人生のドアtoドアは短すぎるな。
35 |
36 | 2つ年上という謎のハンデを背負ってスタートした自分は、最初かなり焦ってました。
37 | 「早く結果を出さなきゃ」「なめられたくない」そんな気持ちで空回りしつつ、必死でした。😓
38 |
39 | 学生時代はあまりうまくいかず、逃げるように沖縄を出て、広島を経てなんだかんだで上京。
40 | 不器用な人生かよって感じですが、「環境を変えた分だけ強くなれる説」は、意外と自分に合っていました。
41 |
42 | Big5性格診断ではNeuroticism(神経質的傾向)が極端に高い自分は、精神的に安定しないまま動きまくるという、取り扱いが難しい性格。
43 | 時には見苦しい姿を見せたこともあったと思いますが、それでも優しく受け止めてくれたみんなには、心から感謝しています。
44 |
45 | 社会人ってやっぱりいろんな壁がありますが、みんなの頑張る姿勢や暖かい笑顔に、現在進行形で勇気とエネルギーをもらい続けています。
46 |
47 | 今こうして振り返ってみるとーー
48 | あの時、沖縄を出て、ここに来て、本当によかったなと思います。😭
49 | もっと早くこの環境に来てたら…とちょっと悔しくなるくらいに。
50 |
51 | 気持ちの転機となったのは、2年目のR社買収による内製チームの強化。
52 | 正直言うと、それまで「この会社でエンジニアとしてやっていくのは厳しいかな」と思ってました(もちろんそのうえでこの会社を選びました)。
53 | でも、内製チームとしてコードを書くようになり、優秀なエンジニアと一緒に働き、もろと一緒にWellComを開発して、、、
54 | 「ん?自分ちゃんとエンジニアとして生きていけるかも?」と、自信と希望が見えてきました。✨
55 |
56 | ## 海外への興味
57 |
58 | - 環境を変えたぶんだけ強くなれる説の信仰
59 | - エンジニアとしてのキャリアに対する確信
60 |
61 | この2つが、「海外に行ってみたい」という好奇心をぐつぐつ煮込んでいきました。
62 |
63 | 「いつかは家族を持って身を固めるためにも、30歳までには海外に挑戦したい!」という思いのもと、カナダの永住権を獲得した先輩のルートを参考に、逆算して準備を始めました。
64 |
65 | 英語?めっちゃできないです。
66 | ということで、英語力底上げのためにフィリピンの語学学校で4ヶ月、本気で勉強してきます。🔥
67 |
68 | ARM?めっちゃ好きです。
69 | 辞めるのはネガティブな理由があったわけではなく、むしろ成長させてくれたからこそ海外への挑戦意欲が生まれたので、オープンワークにはバチくそ良いコメントを残しておきます。
70 |
71 | 本当は、海外大学への進学、独学での英語学習、外資系企業への転職なども考えました。
72 | でも現実には、そこまで頑張りきれなかった自分がいて、手を伸ばせる「エージェント経由でのカナダ渡航」という道を選びました。
73 | 自分に甘いかもしれないけど、完璧を目指さずに、まずは今できる一歩を!という思いです。
74 |
75 | ## 最後に
76 |
77 | これからARMを一緒に盛り上げていくんだ!というタイミングだったので、心残りは小さくありませんし、「また逃げるのか?」と言われても反論できません。
78 | 不安がないわけでもありません。むしろ今までで一番大きなチャレンジになること思っています。
79 |
80 | でも、自分なりに考えぬいた結論です。
81 | 数年後、英語を使いこなせるようになって、いつでもどこでも仕事ができるという自信を手に入れたい。そして、新しい価値観に触れ、人としても深みを増していきたい。
82 | この挑戦を、自分らしく楽しみながら、全力でやりきります。
83 |
84 | よくある「また戻ってくるかもねー」なんて軽口を叩きたいところですが、中途半端な気持ちではこの挑戦に意味がないので、言います。
85 | たぶん、一生戻りません。
86 |
87 | でも、みんなのことは死んでも忘れません。
88 | 「いちゃりばちょーでー(一度会えばみんな兄弟)」という言葉のように、自分にとって唯一の新卒同期11名は、人生の宝物として大切に胸にしまっておきます。
89 |
90 | これからもずっとみんなのことを応援していますし、自分も負けないように頑張ります!
91 | ARMで過ごす時間もあとわずかとなりましたが、引き続きよろしくお願いします!🙇♂️
92 |
93 | *※この記事は公開を限定しています。*
--------------------------------------------------------------------------------
/src/posts/monologue_20250113.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'しんどい時こそ歩きながら考える'
3 | date: '2025-01-13'
4 | tags:
5 | - 'ブログ'
6 | ---
7 |
8 | 不安や悔しさで眠れないことがある。
9 | お腹や頭が痛くなる。
10 | 自分の外にある世界を恨むか、自分自身を恨む。
11 | 不用意に他人と自分を傷つけてしまうこともある。
12 |
13 | やるべきことは頭では分かっているが、体と心がついてこない。
14 | そして、この事実から目を背けようと、他の無関係で簡単なことに時間を費やす。
15 | 酒、ゲーム、他者への依存は、自分にとって負のループの始まりとなることが多い。
16 |
17 | まず、これに気づいたとき「よく気づいたな!自分よ、ハハハ」と言う。
18 | ピンチをチャンスに変える絶好の機会だと思う。
19 | そのあとは、歩きながら考える。(立ち止まって考えるのは絶対無理)
20 |
21 | 他人にごめんなさいをして、自分なんてやっぱり駄目だと考えることは、いっちゃえば簡単だ。
22 | そこで思考を停止している?
23 |
24 | 歩いて考えていると体と心が頭に追いつき、小さなエネルギーで発見がある。
25 | 例えば、周りから無理しすぎだと言われる時、だいたいは自分に期待しすぎなことが多い。とか。
26 | 気温が低いと、なんか嫌な気持ちになってしまうことが多い。とか。
27 | 朝早く起きると、1日の時間がたくさんあるので、頑張らなくてもタスクをこなせることが多い。とか。
28 |
29 | 今日の気づきは、どうやら自分はまだ不完全な自分を認めたくないらしい。
30 | 舐めんな、千年早いわ!
31 | でも大丈夫、一億と二千年経っても人を愛すことはできるって言うし。⛄️
32 |
--------------------------------------------------------------------------------
/src/posts/novel_line.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '【短編】LINE'
3 | date: '2025-03-08'
4 | tags:
5 | - '短編小説'
6 | ---
7 |
8 | ## 起
9 |
10 | LINEが鳴った。
11 |
12 | 「今までありがとう。結婚した」
13 |
14 | いつぶりだろうか。以前付き合っていた彼からLINEが来た。
15 | 2年前に別れて以来、ときどき生存確認のようにLINEが来る。
16 |
17 | 進学を機に遠距離恋愛になることにお互いの不安を話し合った結果、別れるという選択肢を取った。私も浪人で遊べなくなる見込みだったし、先に遠くへ行く彼をずっと惹きつける自信もなかった。
18 | 人として嫌いになったわけではなく、ときどきのLINEのおかげか、今ではその選択は正解だったと感じている。
19 |
20 | 通知がオンになっているのは初期に友達になった彼だけで、LINEが鳴ると、一瞬手が止まる程度には、嬉しい。
21 | だからこそ、今回の内容を見た時に心の中で鳴った不協和音は、いつもより長い間、レポートを書く私の手を止めた。
22 |
23 | でも、返信はすぐに思いついた。
24 |
25 | 「おめでとう。幸せになってね!」
26 |
27 | 返すのは2時間後くらいにしよう。
28 | 中途半端だった心の隙間を空っぽにできると思うと、少しすっきりした。
29 |
30 | ## 承
31 |
32 | あれから3日が経ったが、LINEは鳴らない。既読さえつかない。
33 |
34 | 付き合ってたころから、私たちのLINEのスタンスは、1時間〜2時間くらい空けて返信するという感じに自然となっていた。
35 |
36 | 最初は、結婚したことによって元彼女へLINEする動機がなくなるのは当然だと思っていたが、さすがにちょっと雑な縁の切り方だなーと思い、少しもやもやしてきた。
37 | たとえば、「じゃあね」や「もうLINEは出来ないね」などの言葉が、私は欲しかったのかもしれない。「今までありがとう。結婚した」だけではなく。
38 |
39 | そう考える自分が嫌になる。彼はもう幸せになっているのだ。
40 |
41 | ## 転
42 |
43 | いや、幸せになっていなかったかもしれない。
44 |
45 | 彼のLINEから1週間が経った頃、彼と私と同じ高校で共通の友達だった子からLINEで着信があった。浪人期に残したLINE友達は彼とその子だけだ。講義中だったので気づけなかったが、話し込む時間はなかったので、返信をして次の講義に向かった。
46 |
47 | 「ごめん。今大学で電話はできなさそうなんだけど、何かあった?」
48 |
49 | すぐにその子から返信が来た。
50 |
51 | 「彼と連絡ついてる??」
52 |
53 | 少しイライラしながら、歩きながら返信した。
54 |
55 | 「この前彼から、結婚したってLINEはあったよ!」
56 |
57 | 既読が早い。
58 |
59 | 「結婚??彼大学生でしょ??」
60 | 「彼と全然連絡つかなくて!」
61 | 「もしかしたら先週の飛行機事故にあったのかもと思ってて、、、」
62 |
63 | 続けて飛行機墜落事故を報道するニュースのリンクがついていた。
64 |
65 | 忘れかけてたいろんなことが一瞬で繋がる。本当に冷たい汗というのがあるんだと思うほど、寒気に襲われた。
66 |
67 | 10月26日。彼から最後にLINEがあった日が、ニュース記事の最終更新日と一致している。
68 |
69 | 「今までありがとう。結婚した」
70 |
71 | 結婚の話もそうだけど、文を改めて見ると不自然で、途中で途切れているかのようにも見える。
72 | 送信された時間と墜落時刻が一致したとき、その推測は確信に変わった。
73 |
74 | --- 今までありがとう。結婚したかった!
75 |
76 | 彼は最後に、私にそんなことを伝えたかったのかもしれない。書いている途中に墜落の振動で送信ボタンが押されたのかもしれない。不協和音が前回より大きく心に鳴った。
77 |
78 | 馬鹿!とつっこむほどの余裕はなく、スマホは涙で濡れて反応しなくなっていた。
79 |
80 | ## 結
81 |
82 | 今日であれから1ヶ月が経つ。
83 | 1つのけじめとして、彼を忘れようと思う。
84 | LINEを開いて今度は私から話しかける。
85 |
86 | 「今までありがとう。」
87 |
88 | 結婚したかった!と書いている途中で、それは本心ではないと思い、その部分は消した。
89 |
90 | 私も強くなって前を向かないといけないし、彼はきっと私を恨んだりはしないと思う。もしもう一度彼に会えたら、もしLINEができたら、いつでも素直な想いを言葉にする大切さを学んだことへの感謝を伝えたい。
91 |
92 | 講義が終わると同時に、LINEが鳴った。
93 |
94 | 「なーんてね。今度ご飯でも行かない?」
95 |
96 | 心に鳴る不協和音を遮って叫んだ。
97 |
98 | 「馬鹿!」
99 |
100 | 退席しようとしていた学生たちが一瞬こちらを見て止まり、少ししてまた日常へと戻っていった。
101 |
--------------------------------------------------------------------------------
/src/posts/recaptcha_2_install.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'reCAPTCHA(リキャプチャ)導入'
3 | date: '2021-03-18'
4 | tags:
5 | - 'reCAPTCHA'
6 | - 'PHP'
7 | - 'セキュリティ'
8 | ---
9 |
10 | ## reCAPTCHAとは
11 |
12 | reCAPTCHA(リキャプチャ)とは、CAPTCHAと呼ばれる
13 | ボットの攻撃からWebサイトを守る機能の一種。
14 |
15 | CAPTCHAとは「completely automated public Turing test to tell computers and humans apart 」の略で、人間とコンピュータを区別するためのテストというような意味を持つ。
16 |
17 | 具体的な方法としては、下の画像のように崩れた文字を認識させたり、あてはまる画像を選択させたりする。
18 |
19 | 
20 | 
21 |
22 | 2021年2月現在、reCAPTCHAはv1(提供停止)、v2、v3まで開発されてGoogleが提供している。
23 |
24 | reCAPTCHAについて詳しく書かれた記事はこちら▼
25 | [「reCAPTCHA」って?スパム対策に効果的なreCAPTCHAをフォームに入れてみた](https://www.synergy-marketing.co.jp/blog/using_recaptcha_on_form)
26 |
27 | ## reCAPTCHA v2 導入手順
28 |
29 | クライアント側につける「サイトキー」とサーバー側につける「シークレットキー」によって機能をつけることができる。メール送信フォームを参考に、鍵を作成→クライアント側(HTMLとjQuery)を記述→サーバー側(PHP)を記述という流れで実装する。
30 |
31 | 1. Google reCAPTCHA にアクセス
32 | https://www.google.com/recaptcha/about/
33 | 
34 |
35 | 1. Admin Consoleをクリックしたあと「+」ボタンから新規作成ページへ行く。
36 | 
37 |
38 | 1. 名前、バージョン(今回はv2をチェック)、ドメインをそれぞれ入力して送信。
39 | 
40 |
41 | 1. サイトキーとシークレットキーが表示されるので、閉じずにそのままにしておく。
42 |
43 | 1. HTMLでメール送信フォームを作り、サーバー側でreCAPCHAのデータをチェックする
44 |
45 | ```HTML:html
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
72 |
80 |
81 |
82 | ```
83 |
84 | ```php:php
85 | // メール送信処理の中で
86 |
87 | $recaptcha = h($this->request->data['g-recaptcha-response']);
88 | if (isset($recaptcha)) {
89 | $captcha = $recaptcha;
90 | } else {
91 | $captcha = '';
92 | }
93 | // シークレットキーを入れる
94 | $secretKey = "";
95 | $resp = @file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret={$secretKey}&response={$captcha}");
96 | $resp_result = json_decode($resp,true);
97 | if(intval($resp_result["success"]) !== 1) {
98 | // reCAPTCHA承認失敗時の処理
99 | } else {
100 | // reCAPTCHA承認成功時の処理
101 | }
102 | ```
103 |
104 | これで、v2のreCAPTHAを実装できた。
105 | 
106 |
107 | ただし日々のコンピュータの進化は早く、これらを突破するソフトウェアやreCAPCHTAの脆弱性についてもすでにいくつか報告されている。
108 |
109 | コンピュータと人を見分けるテスト。熱い戦いが続きそう。
110 |
111 | ### 参考にさせていただいた記事
112 |
113 | [Googleの「reCAPTCHA」を5分で実装する](https://liapoc.com/recaptcha.html)
114 |
--------------------------------------------------------------------------------
/src/posts/study_english_in_philippines_4.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'フィリピン留学メモ(4 / 15 weeks)'
3 | date: '2025-09-14'
4 | tags:
5 | - 'ブログ'
6 | ---
7 |
8 |
13 |
14 | 8月から4ヶ月間、フィリピンの語学学校に英語留学をすることにしました。
15 | 目標はIELTSスコア6.5を取ることです。
16 | 英語がほとんど話せない状態で飛び込んだフィリピンでの生活を、記録として残していきます。
17 |
18 | ▼ 前回の記事
19 |
20 | https://furugen-island.com/my_site/blog/study_english_in_philippines_3
21 |
22 | ## Week 4
23 |
24 | ### 勉強法を試行錯誤する
25 |
26 | 今週は、積極的にかつ楽しく英語を勉強するべく、いろいろ試してみました。🙌
27 |
28 | 朝は、スマホアプリで海外ニュースをリスニングしました。
29 | ディクテーション(書き取り)も試しましたが、、ついていけなさすぎて断念。
30 | シャドーイング(復唱)は騒音被害を招くため、試さずに断念。
31 |
32 | また、昼にはChatGPTにスピーキングの試験官になりきってもらい、
33 | 録音した音声を採点・添削してもらいました。
34 | これは楽しいですし、フィードバックがすぐに得られるので続けれそうです。
35 |
36 | リーディングは、ITテック分野に関する記事を読むようにしています。
37 | これは将来に関わることでもあるので続けたいです。
38 | また、IELTS対策としては、設問タイプ別の傾向とコツを調べて、
39 | 模試で意識したり、先生にピンポイントで質問したりしました。
40 |
41 | ライティングは、「もう数をこなすしかない」と割り切り、
42 | 毎日先生に宿題をもらい、構成や文量感を身につける意識で書きました。
43 | 構成に関しては大きな修正をもらわなくなりましたが、まだ文法やスペルのミスが多いので、
44 | 2度同じミスはしない意識で引き続き取り組みたいです。
45 |
46 | ### 残り3ヶ月!
47 |
48 | わ、早い、焦ります。
49 | 2週に1回のペースでIELTS模試があるので、ここで目標スコアを立てました。
50 | この内容は机の前の壁に貼り付けており、随時エネルギーをもらっています(笑)
51 |
52 | ▼ 模試スケジュールと目標スコア
53 |
54 | - 9/12:5.0
55 | - 9/26:5.5
56 | - 10/10:6.0
57 | - 10/24:6.5
58 | - 11/7:6.5
59 | - 11/21:7.0
60 | - 11/28:7.0
61 |
62 | まずは9/12の結果が週明けにもらえるので、5.0には届いててほしいです🌙
63 |
64 | ### Duolingo Web Test
65 |
66 | Duolingo Web Testは、IELTSに似てリスニング、スピーキング、ライティング、リーディングの4技能をはかる試験です。
67 |
68 | ▼ Duolingo Web Test。このスコアはカナダ留学にも使える。
69 |
70 | https://englishtest.duolingo.com/
71 |
72 | ここに来る前に受けた結果、スコアは70点でした。
73 |
74 |
79 |
80 | 1ヶ月が経過した今週、プレテストを受けて90点相当の結果が出すことができました。
81 | (プレテストは、無料かつ1時間弱で受けられる簡易テストです。)
82 | まだまだですが、こうして結果に表れるのは嬉しいです✨
83 |
84 |
89 |
90 | ### 学校生活
91 |
92 | 毎週金曜日には、その週で修了した生徒の卒業式が行われます。
93 | 国籍も期間も目的も多様な方々にお会いできることは、とても良い経験だなーと感じています。
94 |
95 | ▼ グループクラスで1ヶ月共にした方ともお別れ。またどこかで!
96 |
97 |
102 |
103 | ## まとめ
104 |
105 | Week4は、勉強方法をいろいろ試しながら成長の実感を少し感じることができた1週間でした。
106 |
107 | ベースが低かったこともあり、ここからがもっと頑張らないといけない段階だと思うので、モチベーションを維持しながら勉強を習慣化させていきます。
108 | 次週は特にリスニングに力を入れたいと思っています。試験音源はもちろん、先生や友達の会話、ニュースのネイティブをもっと聞き取れるようになりたいです👂
109 |
110 | ## おまけ
111 |
112 | ▼ 先生に教えてもらった、きゅうりフレーバーの炭酸飲料。ノーコメントで。
113 |
114 |
119 |
--------------------------------------------------------------------------------
/src/types/game-objects.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | export type RowType = 'forest' | 'car' | 'truck'
4 |
5 | export type Row =
6 | | {
7 | type: 'forest'
8 | trees: { tileIndex: number; height: number }[]
9 | }
10 | | {
11 | type: 'car'
12 | direction: boolean
13 | speed: number
14 | vehicles: {
15 | initialTileIndex: number
16 | color: THREE.ColorRepresentation
17 | }[]
18 | }
19 | | {
20 | type: 'truck'
21 | direction: boolean
22 | speed: number
23 | vehicles: {
24 | initialTileIndex: number
25 | color: THREE.ColorRepresentation
26 | }[]
27 | }
28 |
29 | export type MoveDirection = 'forward' | 'backward' | 'left' | 'right'
30 |
--------------------------------------------------------------------------------
/src/types/posts.ts:
--------------------------------------------------------------------------------
1 | export type Post = {
2 | slug: string
3 | title: string
4 | date: string
5 | tags: string[]
6 | content: string
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "types": ["node"]
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | "app/types/**/*.ts",
31 | "app/types/**/*.d.ts"
32 | ],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "redirects": [
3 | {
4 | "source": "/",
5 | "destination": "/my_site",
6 | "permanent": true
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------