Feat: add mobile (440-768px) styles

This commit is contained in:
Kentai Radiquum 2025-07-23 11:09:30 +05:00
parent c2d825bf36
commit 5981db3626
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
51 changed files with 817 additions and 133 deletions

View file

@ -0,0 +1,54 @@
import Link from "next/link";
import { IconWithText } from "../components/IconWithText";
import { Section } from "../components/Section";
import { CharacterImage } from "../components/CharacterImage";
const links = [
{
icon: "/icons/furaffinity.svg",
text: "Furaffinity",
desc: "radiquum",
url: "https://furaffinity.net/user/radiquum",
},
{
icon: "/icons/itaku.svg",
text: "Itaku",
desc: "radiquum",
url: "https://itaku.ee/profile/radiquum",
},
];
export const Characters = () => {
return (
<Section>
<div className="flex flex-col gap-2">
<h2 className="text-4xl">Characters</h2>
<div className="flex items-center gap-4">
{links.map((item) => (
<Link href={item.url} key={`photos.link.${item.text}`}>
<IconWithText
icon={item.icon}
text={item.text}
desc={item.desc}
/>
</Link>
))}
</div>
</div>
<div className="flex flex-col gap-4 mt-2">
<CharacterImage
name="Kentai"
species="Red Panda"
gender="Male"
image="/images/red_panda.png"
/>
<CharacterImage
name=""
species="Protogen"
gender="Male"
image="/images/protogen.png"
/>
</div>
</Section>
);
};

62
app/Section/Contacts.tsx Normal file
View file

@ -0,0 +1,62 @@
import Link from "next/link";
import { IconWithText } from "../components/IconWithText";
import { Section } from "../components/Section";
const links = [
{
icon: "/icons/ic_baseline-telegram.svg",
text: "Telegram",
desc: "@radiquum",
url: "https://t.me/radiquum",
},
{
icon: "/icons/ic_baseline-discord.svg",
text: "Discord",
desc: "radiquum",
url: null,
},
{
icon: "/icons/material-symbols_mail.svg",
text: "E-Mail",
desc: "radiquum@wah.su",
url: "mailto:radiquum@wah.su",
},
{
icon: "/icons/ri_vk-fill.svg",
text: "Vkontakte",
desc: "@radiquum",
url: "https://vk.com/radiquum",
},
];
export const Contacts = () => {
return (
<Section>
<h2 className="text-4xl">Contacts</h2>
<div className="grid grid-cols-2 gap-4">
{links.map((item) => {
if (item.url) {
return (
<Link href={item.url} key={`socials.link.${item.text}`}>
<IconWithText
icon={item.icon}
text={item.text}
desc={item.desc}
/>
</Link>
);
} else {
return (
<IconWithText
key={`socials.${item.text}`}
icon={item.icon}
text={item.text}
desc={item.desc}
/>
);
}
})}
</div>
</Section>
);
};

20
app/Section/Intro.tsx Normal file
View file

@ -0,0 +1,20 @@
export const Intro = () => {
return (
<div className="flex gap-4 flex-col">
<div className="w-full flex items-center justify-center py-24 bg-[#101316] rounded-tl-[256px] rounded-br-[256px]">
<h1 className="text-[#FFB1CD] tracking-tight font-bold text-8xl">
Radiquum
</h1>
</div>
<div className="w-full flex items-center justify-center py-24 bg-[#161213] rounded-bl-[128px] rounded-tr-[128px]">
<div className="flex flex-col">
<p className="text-[#C8E8FE] font-medium text-4xl">Photographer</p>
<p className="text-[#FF8686] font-medium text-4xl">Developer</p>
<p className="text-[#FF851A] font-medium text-4xl">
Self-Hosting admirer
</p>
</div>
</div>
</div>
);
};

61
app/Section/Photos.tsx Normal file
View file

@ -0,0 +1,61 @@
"use client";
import Link from "next/link";
import { Section } from "../components/Section";
import { IconWithText } from "../components/IconWithText";
import { EmblaOptionsType } from "embla-carousel";
import EmblaCarousel from "../components/Photos.Carousel";
const links = [
{
icon: "/icons/ic_baseline-telegram.svg",
text: "Telegram",
desc: "@photowah",
url: "https://x.com/radiquum",
},
{
icon: "/icons/simple-icons_immich.svg",
text: "Gallery",
desc: "wah.su/photos",
url: "https://bsky.app/profile/@radiquum.wah.su",
},
];
const OPTIONS: EmblaOptionsType = { dragFree: true, loop: true };
const SLIDES = [
"/images/photos/2024-06-14T19_32_04_024.JPG",
"/images/photos/2024-06-17T18_55_55_030.JPG",
"/images/photos/2024-06-17T19_44_15_068.JPG",
"/images/photos/2024-06-17T19_48_32_074.JPG",
"/images/photos/2024-06-22T20_17_57_001.JPG",
"/images/photos/image_2024-06-02_16-55-04.png",
"/images/photos/IMG_20230604_043911_43.JPG",
"/images/photos/IMG_20240710_225123.jpg",
"/images/photos/IMG_20240710_225448.jpg",
"/images/photos/IMG_20230602_190558.JPG",
"/images/photos/IMG_20230603_175135.JPG",
];
export const Photos = () => {
return (
<div className="flex flex-col gap-4">
<Section>
<div className="flex flex-col gap-2">
<h2 className="text-4xl">Photos</h2>
<div className="flex items-center gap-4">
{links.map((item) => (
<Link href={item.url} key={`photos.link.${item.text}`}>
<IconWithText
icon={item.icon}
text={item.text}
desc={item.desc}
/>
</Link>
))}
</div>
</div>
</Section>
<EmblaCarousel slides={SLIDES} options={OPTIONS} />
</div>
);
};

63
app/Section/Projects.tsx Normal file
View file

@ -0,0 +1,63 @@
import Link from "next/link";
import { IconWithText } from "../components/IconWithText";
import { Section } from "../components/Section";
const links = [
{
icon: "/icons/mdi_github.svg",
text: "Anix",
desc: "Unofficial web client for anixart",
url: "https://github.com/radiquum/AniX",
},
{
icon: "/icons/mdi_github.svg",
text: "Furaffinity-dl",
desc: "Fork with additional functionality",
url: "https://github.com/radiquum/furaffinity-dl",
},
{
icon: "/icons/mdi_github.svg",
text: "TG-Photos",
desc: "Google Photo like bot for Telegram",
url: "https://github.com/radiquum/TG-Photos",
},
{
icon: "/icons/mdi_github.svg",
text: "TIG",
desc: "Generate images from text",
url: "https://github.com/radiquum/TIG",
},
{
icon: "/icons/mdi_github.svg",
text: "GitHub",
desc: "Other Projects",
url: "https://github.com/radiquum",
},
{
icon: "/icons/wahsu.svg",
text: "wah.su",
desc: "Self-Hosting project",
url: "https://wah.su",
},
{
icon: "/icons/ic_baseline-telegram.svg",
text: "Red Pandas Stickers",
desc: "Collection of Red Panda related sticker packs",
url: "https://t.me/red_panda_stickers",
},
];
export const Projects = () => {
return (
<Section>
<h2 className="text-4xl">Projects</h2>
<div className="grid grid-cols-1 gap-2">
{links.map((item) => (
<Link href={item.url} key={`socials.link.${item.text}`}>
<IconWithText icon={item.icon} text={item.text} desc={item.desc} />
</Link>
))}
</div>
</Section>
);
};

63
app/Section/Socials.tsx Normal file
View file

@ -0,0 +1,63 @@
import Link from "next/link";
import { IconWithText } from "../components/IconWithText";
import { Section } from "../components/Section";
const links = [
{
icon: "/icons/uim_twitter.svg",
text: "Twitter",
desc: "@radiquum",
url: "https://x.com/radiquum",
},
{
icon: "/icons/ri_bluesky-fill.svg",
text: "BlueSky",
desc: "@radiquum.wah.su",
url: "https://bsky.app/profile/@radiquum.wah.su",
},
{
icon: "/icons/mdi_mastodon.svg",
text: "Mastodon",
desc: "@radiquum@furry.engineer",
url: "https://furry.engineer/@radiquum",
},
{
icon: "/icons/ic_baseline-telegram.svg",
text: "Telegram",
desc: "@radiquumprojects",
url: "https://t.me/radiquumprojects",
},
{
icon: "/icons/ri_vk-fill.svg",
text: "Vkontakte",
desc: "@radiquum",
url: "https://vk.com/radiquum",
},
{
icon: "/icons/ri_pixelfed-fill.svg",
text: "Pixey",
desc: "@radiquum@pixey.org",
url: "https://pixey.org/@radiquum",
},
{
icon: "/icons/ri_pixelfed-fill.svg",
text: "Instafops",
desc: "@radiquwum@Instafops.net",
url: "https://instafops.net/@radiquum",
},
];
export const Socials = () => {
return (
<Section>
<h2 className="text-4xl">Socials</h2>
<div className="grid grid-cols-2 gap-x-12 gap-y-2">
{links.map((item) => (
<Link href={item.url} key={`socials.link.${item.text}`}>
<IconWithText icon={item.icon} text={item.text} desc={item.desc} />
</Link>
))}
</div>
</Section>
);
};

View file

@ -0,0 +1,28 @@
/* eslint-disable @next/next/no-img-element */
type CharacterImageProps = {
image: string;
name: string | null;
species: string;
gender: string;
};
export const CharacterImage = ({
image,
name,
species,
gender,
}: CharacterImageProps) => {
return (
<div className="relative rounded-4xl overflow-hidden">
<img src={image} alt="" className="rounded-4xl" />
<div className="absolute left-0 right-0 bottom-0 from-[#131314] to-[#131314]/0 bg-gradient-to-t">
<div className="p-4 flex gap-2 justify-between items-end">
<p className="text-5xl">{name}</p>
<p className="flex-1 text-right text-3xl">
{species}, {gender}
</p>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,18 @@
type IconWithTextProps = {
icon: string;
text: string;
desc: string;
};
export const IconWithText = ({ icon, text, desc }: IconWithTextProps) => {
return (
<div className="flex items-center gap-1">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={icon} alt={""} />
<div>
<p className="text-2xl">{text}</p>
<p className="text-sm">{desc}</p>
</div>
</div>
);
};

View file

@ -0,0 +1,112 @@
import Styles from "./Photos.Carousel.module.css";
import React, { useCallback, useEffect, useRef } from "react";
import {
EmblaCarouselType,
EmblaEventType,
EmblaOptionsType,
} from "embla-carousel";
import useEmblaCarousel from "embla-carousel-react";
const TWEEN_FACTOR_BASE = 0.2;
type PropType = {
slides: string[];
options?: EmblaOptionsType;
};
const EmblaCarousel: React.FC<PropType> = (props) => {
const { slides, options } = props;
const [emblaRef, emblaApi] = useEmblaCarousel(options);
const tweenFactor = useRef(0);
const tweenNodes = useRef<HTMLElement[]>([]);
const setTweenNodes = useCallback((emblaApi: EmblaCarouselType): void => {
tweenNodes.current = emblaApi.slideNodes().map((slideNode) => {
return slideNode.querySelector(".embla__parallax__layer") as HTMLElement;
});
}, []);
const setTweenFactor = useCallback((emblaApi: EmblaCarouselType) => {
tweenFactor.current = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length;
}, []);
const tweenParallax = useCallback(
(emblaApi: EmblaCarouselType, eventName?: EmblaEventType) => {
const engine = emblaApi.internalEngine();
const scrollProgress = emblaApi.scrollProgress();
const slidesInView = emblaApi.slidesInView();
const isScrollEvent = eventName === "scroll";
emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {
let diffToTarget = scrollSnap - scrollProgress;
const slidesInSnap = engine.slideRegistry[snapIndex];
slidesInSnap.forEach((slideIndex) => {
if (isScrollEvent && !slidesInView.includes(slideIndex)) return;
if (engine.options.loop) {
engine.slideLooper.loopPoints.forEach((loopItem) => {
const target = loopItem.target();
if (slideIndex === loopItem.index && target !== 0) {
const sign = Math.sign(target);
if (sign === -1) {
diffToTarget = scrollSnap - (1 + scrollProgress);
}
if (sign === 1) {
diffToTarget = scrollSnap + (1 - scrollProgress);
}
}
});
}
const translate = diffToTarget * (-1 * tweenFactor.current) * 100;
const tweenNode = tweenNodes.current[slideIndex];
tweenNode.style.transform = `translateX(${translate}%)`;
});
});
},
[]
);
useEffect(() => {
if (!emblaApi) return;
setTweenNodes(emblaApi);
setTweenFactor(emblaApi);
tweenParallax(emblaApi);
emblaApi
.on("reInit", setTweenNodes)
.on("reInit", setTweenFactor)
.on("reInit", tweenParallax)
.on("scroll", tweenParallax)
.on("slideFocus", tweenParallax);
}, [emblaApi, tweenParallax]);
return (
<div className="embla">
<div className="embla__viewport" ref={emblaRef}>
<div className="embla__container">
{slides.map((index) => (
<div className="embla__slide" key={index}>
<div className="embla__parallax">
<div className="embla__parallax__layer">
<img
className="embla__slide__img embla__parallax__img"
src={index}
alt="Your alt text"
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default EmblaCarousel;

View file

@ -0,0 +1,9 @@
export const Section = ({
children,
}: Readonly<{ children: React.ReactNode }>) => {
return (
<div className="max-w-[400px] w-full mx-auto flex flex-col gap-2">
{children}
</div>
);
};

View file

@ -1,8 +1,8 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #090909;
--foreground: #ededed;
}
@theme inline {
@ -12,15 +12,78 @@
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
@font-face {
font-family: "LTSuperior";
src: url("/fonts/LTSuperior-Mar2025/LTSuperior-Regular.ttf")
format("truetype");
font-weight: 400;
}
@font-face {
font-family: "LTSuperior";
src: url("/fonts/LTSuperior-Mar2025/LTSuperior-Medium.ttf") format("truetype");
font-weight: 500;
}
@font-face {
font-family: "LTSuperior";
src: url("/fonts/LTSuperior-Mar2025/LTSuperior-SemiBold.ttf")
format("truetype");
font-weight: 700;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: LTSuperior, Inter, Helvetica, sans-serif;
}
/* embla styles */
.embla {
max-width: 48rem;
margin: auto;
--slide-height: 144px;
--slide-spacing: 8px;
--slide-size: 256px;
}
.embla__viewport {
overflow: hidden;
}
.embla__container {
display: flex;
touch-action: pan-y pinch-zoom;
margin-left: calc(var(--slide-spacing) * -1);
}
.embla__slide {
transform: translate3d(0, 0, 0);
flex: 0 0 var(--slide-size);
min-width: 0;
padding-left: var(--slide-spacing);
}
.embla__slide__img {
border-radius: 1.8rem;
display: block;
height: var(--slide-height);
width: 100%;
-o-object-fit: cover;
object-fit: cover;
}
.embla__parallax {
border-radius: 1.8rem;
height: 100%;
overflow: hidden;
}
.embla__parallax__layer {
position: relative;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
}
.embla__parallax__img {
max-width: none;
flex: 0 0 calc(115% + (var(--slide-spacing) * 2));
-o-object-fit: cover;
object-fit: cover;
}

View file

@ -1,17 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
@ -24,11 +13,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<body className={`antialiased`}>{children}</body>
</html>
);
}

View file

@ -1,103 +1,23 @@
import Image from "next/image";
import { Characters } from "./Section/Characters";
import { Contacts } from "./Section/Contacts";
import { Intro } from "./Section/Intro";
import { Photos } from "./Section/Photos";
import { Projects } from "./Section/Projects";
import { Socials } from "./Section/Socials";
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
<main className="flex flex-col gap-4 overflow-hidden">
<div className="flex flex-col gap-8 overflow-hidden">
<Intro />
<Socials />
<Photos />
<Projects />
</div>
<div className="flex flex-col gap-8 overflow-hidden bg-[#131314] rounded-t-4xl pt-4 pb-8">
<Characters />
<Contacts />
</div>
</main>
);
}