First Commit

This commit is contained in:
Last2014 2025-07-16 18:07:08 +09:00
commit 8a8067287c
26 changed files with 6545 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Last2014
https://last2014.f5.si
Last2014 Home Website

16
eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

11
next.config.ts Normal file
View File

@ -0,0 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
distDir: process.env.DIST || "./out",
images: {
unoptimized: true,
},
};
export default nextConfig;

5453
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "last2014",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^13.0.0",
"@types/eslint": "^9.6.1",
"@types/next": "^9.0.0",
"bulma": "^1.0.4",
"date-fns": "^4.1.0",
"next": "^15.3.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-fast-marquee": "^1.6.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.0",
"@types/node": "^20",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"typescript": "^5"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/last2014.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

65
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,65 @@
import Link from "next/link";
import { Icon } from "@iconify/react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Site | Last2014",
description: "About Last2014 Website",
};
import { Noto_Sans_JP } from "next/font/google";
const Font = Noto_Sans_JP({ subsets: ["latin"] });
export default function PrivacyPolicy() {
return (
<div
className={Font.className + " section container"}
style={{ padding: "0" }}
>
<div className="is-flex is-align-items-center">
<Link href="/" className="button" style={{ marginBottom: "1rem" }}>
<Icon icon="octicon:home-fill-16" style={{ marginRight: "5px" }} />
Back to Home
</Link>
</div>
<div className="title is-2 is-flex is-align-items-center">
<Icon icon="ix:about-filled" />
</div>
<div className="content">
<h2 className="title is-4"></h2>
<p>
</p>
<h2 className="title is-4"></h2>
<p>
<br />
<Link href="/contact"></Link>
<p style={{ fontSize: "0.6rem" }}></p>
</p>
<h2></h2>
<p>
使
<code>{"<img>"}</code>
</p>
</div>
<div className="is-flex is-align-items-center">
<Icon icon="material-symbols:alarm-on-rounded" />
2025/06/16
</div>
</div>
);
}

35
src/app/error.tsx Normal file
View File

@ -0,0 +1,35 @@
"use client";
import Link from "next/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Process Error | Last2014",
description: "Error Page",
};
export default function Error() {
return (
<div className="has-text-centered">
<h1 className="title">Process Error - HTTP500</h1>
<div
className="notification is-warning"
style={{
width: "15em",
margin: "0 auto",
padding: "0.5rem",
}}
>
<h3>
{"An error has occurred"}
</h3>
</div>
<Link className="button" style={{ marginTop: "10px" }} href="/">
Go to Home
</Link>
</div>
);
}

46
src/app/globals.css Normal file
View File

@ -0,0 +1,46 @@
@import "bulma/css/bulma.min.css";
* {
margin: 0;
padding: 0;
border-radius: 0;
}
body {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height: 100%;
display: flex;
flex-direction: column;
min-height: 100vh;
font-weight: 100;
font-style: normal;
}
.aboutme,
.skills,
.profile {
--bulma-card-media-margin: 0.5rem;
}
.footer {
padding: 0;
width: 100%;
}
.nomalLink {
color: var(--bulma-body-color);
}
.nomalLink:hover {
text-decoration: underline;
}
main {
flex: 1;
padding: 1rem;
}

30
src/app/layout.tsx Normal file
View File

@ -0,0 +1,30 @@
import type { Metadata } from "next";
import "./globals.css";
import Footer from "@/components/footer";
export const metadata: Metadata = {
title: "Last2014",
description: "Last2014 Website",
};
import { Roboto } from 'next/font/google';
const Font = Roboto({ subsets: ['latin'] });
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html data-theme="dark">
<body className={Font.className}>
<main>
{children}
</main>
<Footer />
</body>
</html>
);
}

View File

@ -0,0 +1,35 @@
import { notFound, redirect } from "next/navigation";
const serviceUrlMap = {
blog: "https://blog.last2014.com",
chiebukuro: "https://chiebukuro.yahoo.co.jp/user/1154047737",
gitea: "https://gitea.last2014.f5.si/last2014",
nicovideo: "https://www.nicovideo.jp/user/140339612",
qiita: "https://qiita.com/last2014",
zenn: "https://zenn.dev/last2014",
wakatime: "https://wakatime.com/@last2014",
mail: "mailto:last2014yh@yahoo.co.jp",
} as const;
type ServiceKey = keyof typeof serviceUrlMap;
export function generateStaticParams() {
return Object.keys(serviceUrlMap).map((service) => ({
service: service as ServiceKey,
}));
}
export default async function Links({
params,
}: {
params: Promise<{ service: ServiceKey }>;
}) {
const { service } = await params;
const url = serviceUrlMap[service];
if (!url) {
return notFound();
}
redirect(url);
}

35
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,35 @@
import Link from "next/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Not Found | Last2014",
description: "Not Found Page",
};
export default function NotFound() {
return (
<div className="has-text-centered">
<h1 className="title">Not Found - HTTP404</h1>
<div
className="notification is-warning"
style={{
width: "15em",
margin: "0 auto",
padding: "0.5rem",
}}
>
<h3>
{"The page you requested"}
<br />
{"could not be found"}
</h3>
</div>
<Link className="button" style={{ marginTop: "10px" }} href="/">
Go to Home
</Link>
</div>
);
}

24
src/app/page.tsx Normal file
View File

@ -0,0 +1,24 @@
import "./style.css";
// Cards
import TopProfile from "@/cards/profile";
import AboutMe from "@/cards/aboutme";
import Skill from "@/cards/skill";
import Links from "@/cards/links";
import Details from "@/cards/details";
export default function Home() {
return (
<>
<TopProfile />
<AboutMe />
<Skill />
<Links />
<Details />
</>
);
}

View File

@ -0,0 +1,99 @@
import Link from "next/link";
import { Icon } from "@iconify/react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy Policy | Last2014",
description: "Last2014 Website Privacy Policy",
};
import { Noto_Sans_JP } from "next/font/google";
const Font = Noto_Sans_JP({ subsets: ["latin"] });
export default function PrivacyPolicy() {
return (
<div
className={Font.className + " section container"}
style={{ padding: "0" }}
>
<div className="is-flex is-align-items-center">
<Link href="/" className="button" style={{ marginBottom: "1rem" }}>
<Icon icon="octicon:home-fill-16" style={{ marginRight: "5px" }} />
Back to Home
</Link>
</div>
<div className="title is-2 is-flex is-align-items-center">
<Icon icon="eos-icons:network-policy" />
</div>
<div className="content">
<h2 className="title is-4"></h2>
<p>
</p>
<h2 className="title is-4"></h2>
<p>
Cloudflare
Tunnelを経由してサービスを提供していますCloudflare Web
AnalyticsCloudflare Browser
Insightsを利用してアクセス解析を行っています
Cloudflareに送信される可能性があります
<a
href="https://www.cloudflare.com/privacypolicy/"
className="nomalLink is-underlined"
target="_blank"
rel="noopener noreferrer"
>
Cloudflareのプライバシーポリシー
</a>
</p>
<p>
IPアドレスや国
</p>
<p>
調
</p>
<h2 className="title is-4"></h2>
<p>
Cloudflare Web AnalyticsCloudflare Browser
Insightsを利用してアクセス解析を行っています
使
</p>
<h2 className="title is-4">Cookieについて</h2>
<p>
Cloudflareによるアクセス解析ではCookieを使用する場合があります
<a
href="https://www.cloudflare.com/cookie-policy/"
className="nomalLink is-underlined"
target="_blank"
rel="noopener noreferrer"
>
CloudflareのCookieポリシー
</a>
</p>
<h2 className="title is-4"></h2>
<p>
<Link href="/contact"></Link>
</p>
</div>
<div className="is-flex is-align-items-center">
<Icon icon="material-symbols:alarm-on-rounded" />
2025/06/16
</div>
</div>
);
}

21
src/app/style.css Normal file
View File

@ -0,0 +1,21 @@
.card {
width: 40em;
display: block;
margin: 0 auto;
margin-top: 1em;
margin-bottom: 1em;
}
.topCard {
margin-top: 3em;
}
.media-content {
overflow: hidden;
word-wrap: break-word;
word-break: break-all;
}
#icon {
border-radius: 100%;
}

29
src/cards/aboutme.tsx Normal file
View File

@ -0,0 +1,29 @@
import { Icon } from "@iconify/react";
export default function AboutMe() {
return (
<div className="card card-content aboutme">
<div className="media media-content">
<div
className="is-flex is-align-items-center"
style={{
gap: "0.5em",
}}
>
<Icon
className="title is-4"
style={{ margin: "0" }}
icon="iconoir:people-tag"
/>
<span className="title is-4">About Me</span>
</div>
</div>
<div className="content">
{
"A programmer in elementary school. I'm playing around with Next.js and home servers."
}
</div>
</div>
);
}

144
src/cards/details.tsx Normal file
View File

@ -0,0 +1,144 @@
"use client";
import { useState } from "react";
import { Icon } from "@iconify/react";
import { format, differenceInYears, formatDistanceToNow } from "date-fns";
export default function Details() {
const [open, setOpen] = useState(false);
const [limit, setLimit] = useState(2);
const showMore = () => {
setLimit((prev) => prev + 2);
};
const birthday = new Date("2014-12-8");
const birthdayStr = format(birthday, "MMMM d, yyyy");
const age = differenceInYears(new Date(), birthday);
const startday = new Date("2025-5-31");
const startdayStr = format(startday, "MMMM d, yyyy");
const activitytimes = formatDistanceToNow(startday, { addSuffix: true });
type DetailItem = {
icon: string;
name: string;
value?: string;
};
const details: DetailItem[] = [
{
icon: "iconoir:calendar",
name: "Start of activities",
value: `${startdayStr}(${activitytimes})`,
},
{
icon: "iconoir:birthday-cake",
name: "Birthday",
value: `${birthdayStr}(${age}years old)`,
},
{
icon: "guidance:children-must-be-supervised",
name: "Age",
value: `${age}years old`,
},
{
icon: "iconoir:map-pin",
name: "Location",
value: "Kanagawa, Japan",
},
{
icon: "guidance:unisex-restroom",
name: "Gender",
value: "Male",
},
{
icon: "iconoir:globe",
name: "From",
value: "Japan",
},
{
icon: "iconoir:translate",
name: "Language",
value: "Japanese / English",
},
];
const visibleDetails = details.slice(0, limit);
const hasMore = limit < details.length;
return (
<div className="card card-content links">
<div className="is-flex is-align-items-center" style={{ gap: "0.5em" }}>
<Icon
className="title is-4"
style={{ margin: "0" }}
icon="iconoir:info-circle"
/>
<span className="title is-4" style={{ margin: 0 }}>
Details
</span>
<span
style={{ marginLeft: "auto", cursor: "pointer" }}
onClick={() => setOpen((prev) => !prev)}
>
<Icon
className="title is-4"
icon={open ? "iconoir:nav-arrow-down" : "iconoir:nav-arrow-right"}
/>
</span>
</div>
{open && (
<div className="content links-content" style={{ marginTop: "1em" }}>
{visibleDetails.map((item, idx) => (
<div
key={idx}
className="card card-content links"
style={{ marginBottom: "0.5em" }}
>
<div
className="is-flex is-align-items-center"
style={{ gap: "0.5em" }}
>
<Icon
className="title is-4"
style={{ margin: "0" }}
icon={item.icon}
/>
<span className="title is-4" style={{ margin: 0 }}>
{item.name}
</span>
{item.value && (
<span
style={{
marginLeft: "auto",
fontSize: "0.9em",
color: "#888",
}}
>
{item.value}
</span>
)}
</div>
</div>
))}
{hasMore && (
<div
className="is-flex is-justify-content-center"
style={{ marginTop: "0.5em" }}
>
<button
onClick={showMore}
className="button card is-4"
style={{ width: "auto" }}
>
Load More
</button>
</div>
)}
</div>
)}
</div>
);
}

173
src/cards/links.tsx Normal file
View File

@ -0,0 +1,173 @@
"use client";
import { useState } from "react";
import { Icon } from "@iconify/react";
type LinkItem = {
icon: string;
name: string;
about?: string;
url?: string;
};
const links: LinkItem[] = [
{
icon: "octicon:home-fill-16",
name: "Home Page",
about: "This Page",
url: "/",
},
{
icon: "material-symbols:text-ad",
name: "Blog",
about: "Last2014's Blog",
url: "/links/blog",
},
{
icon: "simple-icons:qiita",
name: "Qiita",
about: "Qiita Profile",
url: "/links/qiita",
},
{
icon: "simple-icons:zenn",
name: "Zenn",
about: "Zenn Profile",
url: "/links/zenn",
},
{
icon: "simple-icons:gitea",
name: "Gitea",
about: "Public Sourcecode",
url: "/links/gitea",
},
{
icon: "jam:yahoo",
name: "Yahoo! Chiebukuro",
about: "Question & Answer",
url: "/links/chiebukuro",
},
{
icon: "simple-icons:niconico",
name: "Nicovideo",
about: "Upload & Watch",
url: "/links/nicovideo",
},
{
icon: "simple-icons:wakatime",
name: "WakaTime",
about: "Working Time",
url: "/links/wakatime",
},
{
icon: "iconoir:mail-solid",
name: "Mail",
about: "Public Mail",
url: "/links/mail",
},
];
export default function Link() {
const [open, setOpen] = useState(false);
const [limit, setLimit] = useState(2);
const showMore = () => {
setLimit((prev) => prev + 2);
};
const visibleLinks = links.slice(0, limit);
const hasMore = limit < links.length;
return (
<div className="card card-content links">
<div className="is-flex is-align-items-center" style={{ gap: "0.5em" }}>
<Icon
className="title is-4"
style={{ margin: "0" }}
icon="iconoir:link"
/>
<span className="title is-4" style={{ margin: 0 }}>
Links
</span>
<span
style={{ marginLeft: "auto", cursor: "pointer" }}
onClick={() => setOpen((prev) => !prev)}
>
<Icon
className="title is-4"
icon={open ? "iconoir:nav-arrow-down" : "iconoir:nav-arrow-right"}
/>
</span>
</div>
{open && (
<div className="content links-content" style={{ marginTop: "1em" }}>
{visibleLinks.map((item, idx) => {
const iconElement = (
<Icon
className="title is-4"
style={{ margin: "0" }}
icon={item.icon}
/>
);
return (
<a
key={idx}
href={item.url}
target={item.url?.startsWith("http") ? "_blank" : undefined}
rel={
item.url?.startsWith("http")
? "noopener noreferrer"
: undefined
}
className="card card-content links"
style={{
marginBottom: "0.5em",
display: "block",
textDecoration: "none",
color: "inherit",
}}
>
<div
className="is-flex is-align-items-center"
style={{ gap: "0.5em" }}
>
{iconElement}
<span className="title is-4" style={{ margin: 0 }}>
{item.name}
</span>
{item.about && (
<span
style={{
marginLeft: "auto",
fontSize: "0.9em",
color: "#888",
}}
>
{item.about}
</span>
)}
</div>
</a>
);
})}
{hasMore && (
<div
className="is-flex is-justify-content-center"
style={{ marginTop: "0.5em" }}
>
<button
onClick={showMore}
className="button card is-4"
style={{ width: "auto" }}
>
Load More
</button>
</div>
)}
</div>
)}
</div>
);
}

41
src/cards/profile.tsx Normal file
View File

@ -0,0 +1,41 @@
import Image from "next/image";
import { Icon } from "@iconify/react";
export default function TopProfile() {
return (
<div className="card card-content topCard profile">
<div className="media">
<div className="media-left">
<figure className="image is-48x48">
<Image
id="icon"
src="/last2014.png"
alt="Last2014 Icon"
width="100"
height="100"
/>
</figure>
</div>
<div className="media-content">
<p className="title is-4">Last2014</p>
<p className="subtitle is-6">@last2014</p>
</div>
</div>
<div className="content is-flex is-align-items-center">
<span style={{ marginRight: "5px" }}>
<Icon icon="hugeicons:computer-programming-01" />
Web Developer
</span>
{"/"}
<span style={{ marginLeft: "5px" }}>
<Icon icon="ph:student-fill" />
Elementary School Student
</span>
</div>
</div>
);
}

11
src/cards/skill.css Normal file
View File

@ -0,0 +1,11 @@
.SkillIcon {
--SkillIconMargin: 5px;
margin-right: var(--SkillIconMargin);
margin-left: var(--SkillIconMargin);
color: #ffffff;
}
.skillsContainer {
display: flex;
gap: 0;
}

62
src/cards/skill.tsx Normal file
View File

@ -0,0 +1,62 @@
import * as SimpleIcons from "@icons-pack/react-simple-icons";
import { Icon } from "@iconify/react";
const Si = SimpleIcons;
const icons = [
Si.SiHtml5,
Si.SiCss,
Si.SiJavascript,
Si.SiTypescript,
Si.SiPhp,
Si.SiReact,
Si.SiNextdotjs,
Si.SiVuedotjs,
Si.SiNodedotjs,
Si.SiBulma,
Si.SiGit,
Si.SiGitea,
Si.SiCloudflare,
];
const repeat = 3;
const iconList = Array.from({ length: repeat }).flatMap(() => icons);
import Marquee from "react-fast-marquee";
import "./skill.css";
export default function Skill() {
return (
<div className="card card-content skills">
<div
className="is-flex is-align-items-center"
style={{
gap: "0.5em",
marginBottom: "var(--bulma-card-media-margin)",
}}
>
<Icon
className="title is-4"
style={{ margin: "0" }}
icon="iconoir:terminal-tag"
/>
<span className="title is-4">Skills</span>
</div>
<div className="content skillsContainer">
<div className="content">Use these to create websites etc.</div>
<Marquee
gradientWidth={70}
gradientColor="#14161a"
gradient
className="has-text-centered"
>
{iconList.map((Icon, i) => (
<Icon className="SkillIcon" size={30} key={i} />
))}
</Marquee>
</div>
</div>
);
}

34
src/components/footer.tsx Normal file
View File

@ -0,0 +1,34 @@
import { getYear } from "date-fns";
import Link from "next/link";
import { Icon } from "@iconify/react";
function getYears(start: number) {
const currentYear = getYear(new Date());
if (currentYear > start) {
return `${start}-${currentYear}`;
} else {
return String(start);
}
}
export default function Footer() {
return (
<footer className="footer has-text-centered">
<p>&copy; {getYears(2025)} Last2014</p>
<p>
<Link className="nomalLink" href="/about">
<Icon icon="ix:about-filled" />
About
</Link>
{" / "}
<Link className="nomalLink" href="/privacypolicy">
<Icon icon="eos-icons:network-policy" />
Privacy Policy
</Link>
</p>
</footer>
);
}

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
"./out/types/**/*.ts",
".next/types/**/*.ts",
"next-env.d.ts",
"./aaa/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}