Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/(portfolio)/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { PRByOrg } from "@/components/portfolio/prs-by-org-section"
import { createAPIClient } from "@/lib/utils/api-client"
import { verifyUsername } from "@/lib/utils/user"
import { getGithubUsernameByCustomSlug } from "@/lib/utils/custom-url"
import { extractSubdomainFromHostname } from "@/lib/utils/domain"
import { headers } from "next/headers"

interface PageProps {
params: Promise<{ username: string }>
Expand Down Expand Up @@ -95,8 +97,14 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>

export default async function PortfolioPage({ params, searchParams }: PageProps) {
const { username: rawUsername } = await params
const hostname = (await headers()).get("host") || ""
const subdomain = extractSubdomainFromHostname(hostname)
if(!subdomain){
return
}
const { layout } = await searchParams
const username = await resolveUsername(rawUsername)

const username = await resolveUsername(rawUsername || subdomain)

if (!username) {
notFound()
Expand Down
70 changes: 36 additions & 34 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ export default function LandingPage() {
})

setIsLoading(true)
router.push(`/${username.trim()}`)
const targetUrl = `${window.location.protocol}//${username}.${window.location.host}/`

window.location.href = targetUrl
}

return (
Expand All @@ -200,36 +202,36 @@ export default function LandingPage() {
rel="noreferrer"
className="hidden md:flex items-center justify-center gap-1.5 outline-none transition-colors border border-transparent text-white px-2.5 py-1.5 rounded-full bg-gray-900 hover:bg-gray-700 active:bg-gray-600 text-xs md:text-sm lg:px-4 lg:py-2.5 lg:text-base tracking-normal whitespace-nowrap cursor-pointer relative group overflow-visible"
>
<FaGithub className="h-4 w-4 relative z-10 transition-transform group-hover:scale-110" />
<span className="relative z-10">GitHub</span>
{displayedStars > 0 && (
<span className="relative z-10 flex items-center gap-1 text-yellow-400 font-medium">
<FaStar className="h-3 w-3" />
<span className="tabular-nums">{displayedStars.toLocaleString()}</span>
</span>
)}
<FaStar className="absolute h-3 w-3 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300 -top-1 -right-1 animate-sparkle" />
<FaStar className="absolute h-2 w-2 text-yellow-300 opacity-0 group-hover:opacity-100 transition-opacity duration-500 -bottom-0.5 -left-0.5 animate-sparkle-float" style={{ animationDelay: '0.2s' }} />
<FaStar className="absolute h-2.5 w-2.5 text-yellow-500 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -left-2 animate-sparkle" style={{ animationDelay: '0.4s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -right-2 animate-sparkle-float" style={{ animationDelay: '0.6s' }} />
<FaGithub className="h-4 w-4 relative z-10 transition-transform group-hover:scale-110" />
<span className="relative z-10">GitHub</span>
{displayedStars > 0 && (
<span className="relative z-10 flex items-center gap-1 text-yellow-400 font-medium">
<FaStar className="h-3 w-3" />
<span className="tabular-nums">{displayedStars.toLocaleString()}</span>
</span>
)}
<FaStar className="absolute h-3 w-3 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300 -top-1 -right-1 animate-sparkle" />
<FaStar className="absolute h-2 w-2 text-yellow-300 opacity-0 group-hover:opacity-100 transition-opacity duration-500 -bottom-0.5 -left-0.5 animate-sparkle-float" style={{ animationDelay: '0.2s' }} />
<FaStar className="absolute h-2.5 w-2.5 text-yellow-500 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -left-2 animate-sparkle" style={{ animationDelay: '0.4s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -right-2 animate-sparkle-float" style={{ animationDelay: '0.6s' }} />
</Link>
<Link
href="https://github.com/kartiklabhshetwar/foliox"
target="_blank"
rel="noreferrer"
className="md:hidden flex items-center justify-center gap-1 outline-none transition-colors border border-transparent text-white px-2.5 py-1.5 rounded-full bg-gray-900 hover:bg-gray-700 active:bg-gray-600 text-xs tracking-normal whitespace-nowrap cursor-pointer relative group overflow-visible"
>
<FaGithub className="h-4 w-4 relative z-10 transition-transform group-hover:scale-110" />
{displayedStars > 0 && (
<span className="relative z-10 flex items-center gap-0.5 text-yellow-400 text-[10px] font-medium">
<FaStar className="h-2.5 w-2.5" />
<span className="tabular-nums">{displayedStars > 999 ? `${(displayedStars / 1000).toFixed(1)}k` : displayedStars}</span>
</span>
)}
<FaStar className="absolute h-2.5 w-2.5 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300 -top-1 -right-1 animate-sparkle" />
<FaStar className="absolute h-2 w-2 text-yellow-300 opacity-0 group-hover:opacity-100 transition-opacity duration-500 -bottom-0.5 -left-0.5 animate-sparkle-float" style={{ animationDelay: '0.2s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-500 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -left-2 animate-sparkle" style={{ animationDelay: '0.4s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -right-2 animate-sparkle-float" style={{ animationDelay: '0.6s' }} />
<FaGithub className="h-4 w-4 relative z-10 transition-transform group-hover:scale-110" />
{displayedStars > 0 && (
<span className="relative z-10 flex items-center gap-0.5 text-yellow-400 text-[10px] font-medium">
<FaStar className="h-2.5 w-2.5" />
<span className="tabular-nums">{displayedStars > 999 ? `${(displayedStars / 1000).toFixed(1)}k` : displayedStars}</span>
</span>
)}
<FaStar className="absolute h-2.5 w-2.5 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300 -top-1 -right-1 animate-sparkle" />
<FaStar className="absolute h-2 w-2 text-yellow-300 opacity-0 group-hover:opacity-100 transition-opacity duration-500 -bottom-0.5 -left-0.5 animate-sparkle-float" style={{ animationDelay: '0.2s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-500 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -left-2 animate-sparkle" style={{ animationDelay: '0.4s' }} />
<FaStar className="absolute h-2 w-2 text-yellow-400 opacity-0 group-hover:opacity-100 transition-opacity duration-500 top-1/2 -right-2 animate-sparkle-float" style={{ animationDelay: '0.6s' }} />
</Link>
</div>
</div>
Expand All @@ -253,19 +255,19 @@ export default function LandingPage() {
<div className="flex w-full flex-col items-center justify-center">
<div className="z-10 flex w-full max-w-[826px] flex-col items-center justify-center gap-8 md:gap-[50px]">
{/* Logo and Description */}
<div className="flex w-full flex-col items-center justify-center text-center">
<div className="flex w-full flex-col items-center justify-center text-center">
<div className="flex flex-col items-center gap-3 md:gap-8">
<div className="flex flex-col items-center gap-3 md:gap-8">
<div className="flex flex-col items-center gap-3 md:gap-8">
<h1 className="text-5xl md:text-7xl font-bold text-white">
<span className="font-[var(--font-playfair)] italic font-normal">folio</span>
<span className="font-sans">x</span>
</h1>
</div>
<p className="text-base md:text-2xl px-5 md:px-10 font-normal text-white tracking-normal opacity-80 max-w-2xl">
Turn your GitHub into a stunning portfolio. Powered by AI, zero coding required.
</p>
<h1 className="text-5xl md:text-7xl font-bold text-white">
<span className="font-[var(--font-playfair)] italic font-normal">folio</span>
<span className="font-sans">x</span>
</h1>
</div>
<p className="text-base md:text-2xl px-5 md:px-10 font-normal text-white tracking-normal opacity-80 max-w-2xl">
Turn your GitHub into a stunning portfolio. Powered by AI, zero coding required.
</p>
</div>
</div>

{/* Form */}
<div className="max-w-md mx-auto relative group w-full">
Expand Down
39 changes: 31 additions & 8 deletions components/portfolio/share-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,39 @@ export function ShareButton({ username }: ShareButtonProps) {
const [availabilityStatus, setAvailabilityStatus] = useState<"idle" | "available" | "taken" | "checking">("idle")
const [registeredSlug, setRegisteredSlug] = useState<string | null>(null)

function getRootHost() {
if (typeof window === "undefined") return "";

const hostParts = window.location.hostname.split(".");

if (hostParts[hostParts.length - 1] === "localhost") {
return "localhost" + (window.location.port ? `:${window.location.port}` : "");
}

return hostParts.slice(-2).join(".") + (window.location.port ? `:${window.location.port}` : "");
}


useEffect(() => {
if (typeof window !== 'undefined') {
const baseUrl = window.location.origin
const layout = searchParams.get("layout")
const params = new URLSearchParams()

if (layout) {
params.set("layout", layout)
}

const queryString = params.toString()
const urlPath = registeredSlug ? `/${registeredSlug}` : `/${username}`
const fullUrl = queryString ? `${baseUrl}${urlPath}?${queryString}` : `${baseUrl}${urlPath}`

const subdomain = registeredSlug ?? username
const protocol = window.location.protocol

const rootDomain = process.env.NEXT_PUBLIC_SITE_URL?.replace(/^https?:\/\//, "") || "localhost:3000";
const fullUrl = `${protocol}//${subdomain}.${rootDomain}${queryString ? `/?${queryString}` : "/"}`;


setPortfolioUrl(fullUrl)

setCanShare(typeof navigator !== 'undefined' && 'share' in navigator)
}
}, [username, registeredSlug, searchParams])
Expand Down Expand Up @@ -190,18 +208,23 @@ export function ShareButton({ username }: ShareButtonProps) {
<div className="flex gap-2">
<div className="flex-1 flex items-center border border-input rounded-md bg-background overflow-hidden focus-within:ring-1 focus-within:ring-ring">
<span className="px-3 text-sm text-muted-foreground whitespace-nowrap border-r border-input bg-muted/50 py-2 max-w-[120px] sm:max-w-[200px] overflow-hidden text-ellipsis">
{typeof window !== 'undefined' ? new URL(window.location.href).origin : ''}/
{typeof window !== 'undefined' ? new URL(window.location.href).protocol : ''}//
</span>
<Input
value={customSlug}
onChange={(e) => {
const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')
setCustomSlug(value)
}}
placeholder="your-custom-url"
placeholder="your-custom-subdomain"
className="border-0 rounded-none focus-visible:ring-0 font-mono text-sm flex-1"
disabled={isRegistering}
/>
<span className="px-3 text-sm text-muted-foreground whitespace-nowrap border-r border-input bg-muted/50 py-2 max-w-[120px] sm:max-w-[200px] overflow-hidden text-ellipsis">
{
getRootHost()
}/
</span>
</div>
<Button
onClick={handleRegister}
Expand Down Expand Up @@ -245,7 +268,7 @@ export function ShareButton({ username }: ShareButtonProps) {
<FaCheck className="h-4 w-4 mt-0.5 shrink-0" />
<span>
Custom URL created successfully! Your portfolio is now available at{" "}
<span className="font-mono font-semibold">/{registeredSlug}</span>
<span className="font-mono font-semibold">{registeredSlug}</span>
</span>
</p>
</div>
Expand Down
56 changes: 56 additions & 0 deletions lib/utils/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Settings } from "../config/settings";
import { type NextRequest, NextResponse } from 'next/server';

const URLMATCH_REGEX = /http:\/\/([^.]+)\.localhost/

export const protocol = Settings.NODE_ENV === "production" ? "https" : "http"
export const rootDomain = Settings.NEXT_PUBLIC_SITE_URL || "localhost:3000"

export function extractSubdomainFromRequest(request: NextRequest): string | null {
const url = request.url;
const host = request.headers.get('host') || '';
const hostname = host.split(':')[0];

if (url.includes('localhost') || url.includes('127.0.0.1')) {
const fullUrlMatch = url.match(URLMATCH_REGEX);
if (fullUrlMatch && fullUrlMatch[1]) {
return fullUrlMatch[1];
}

if (hostname.includes('.localhost')) {
return hostname.split('.')[0];
}

return null;
}

const rootDomainFormatted = rootDomain.split(':')[0];

if (hostname.includes('---') && hostname.endsWith('.foliox.site')) {
const parts = hostname.split('---');
return parts.length > 0 ? parts[0] : null;
}

const isSubdomain =
hostname !== rootDomainFormatted &&
hostname !== `www.${rootDomainFormatted}` &&
hostname.endsWith(`.${rootDomainFormatted}`);

return isSubdomain ? hostname.replace(`.${rootDomainFormatted}`, '') : null;
}

export function extractSubdomainFromHostname(host: string): string {
if (!host) return null

if (host.includes(".localhost")) {
const parts = host.split(".")
return parts[0] || null
}

const parts = host.split(".")
if (parts.length > 2) {
return parts[0]
}

return null
}
Loading