first commit
This commit is contained in:
commit
2d54228143
28 files changed
+8067
No files matched your search
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
// 1. Define the TypeScript interface for a GitHub User object
|
||||
interface GitHubUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
const URL = "https://api.github.com/users";
|
||||
|
||||
const FetchGitUsers = () => {
|
||||
// 2. Add types to state hooks
|
||||
const [users, setUsers] = useState<GitHubUser[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 3. Native Web API AbortController for fetch cleanup
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(URL, { signal: controller.signal });
|
||||
console.log(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: GitHubUser[] = await response.json();
|
||||
setUsers(data);
|
||||
} catch (err: unknown) {
|
||||
// Ignore abort errors caused by component unmounting
|
||||
if (err instanceof Error && err.name === "AbortError") return;
|
||||
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError("An unexpected error occurred.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
// 4. Clean up ongoing fetch if component unmounts
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// DaisyUI Loading Spinner State
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-3">
|
||||
<span className="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p className="text-base-content/70 text-sm font-medium">
|
||||
Loading GitHub Users...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// DaisyUI Alert Error State
|
||||
if (error) {
|
||||
return (
|
||||
<div className="max-w-md mx-auto my-8">
|
||||
<div className="alert alert-error shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container mx-auto p-6 max-w-5xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-base-300 pb-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-base-content tracking-tight">
|
||||
GitHub Users
|
||||
</h2>
|
||||
<p className="text-sm text-base-content/60 mt-1">
|
||||
Fetched directly from GitHub API
|
||||
</p>
|
||||
</div>
|
||||
<div className="badge badge-primary badge-outline font-semibold">
|
||||
{users.length} Users
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Grid */}
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{users.map((user) => {
|
||||
const { id, login, avatar_url, html_url } = user;
|
||||
return (
|
||||
<li key={id}>
|
||||
{/* DaisyUI Card Component */}
|
||||
<div className="card bg-base-100 shadow-md border border-base-200 hover:shadow-xl transition-all duration-200">
|
||||
<figure className="px-6 pt-6">
|
||||
{/* DaisyUI Avatar Container */}
|
||||
<div className="avatar">
|
||||
<div className="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||
{/* <img src={avatar_url} alt={`${login}'s avatar`} /> */}
|
||||
<Image
|
||||
src={avatar_url}
|
||||
alt={`${login}'s avatar`}
|
||||
width={600}
|
||||
height={600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div className="card-body items-center text-center p-5">
|
||||
<h3 className="card-title text-lg font-bold capitalize text-base-content">
|
||||
{login}
|
||||
</h3>
|
||||
<div className="card-actions mt-3 w-full">
|
||||
<a
|
||||
href={html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-primary btn-sm btn-block font-medium"
|
||||
>
|
||||
View Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchGitUsers;
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UserProfileClient() {
|
||||
// 1. Read dynamic route params from context using TypeScript generics
|
||||
const params = useParams<{ userId: string }>();
|
||||
const userId = params.userId;
|
||||
|
||||
const [isFollowing, setIsFollowing] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="card w-96 bg-base-100 shadow-xl border border-base-200">
|
||||
<div className="card-body">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="card-title text-base-content">Client Component</h2>
|
||||
<span className="badge badge-accent badge-outline">useParams()</span>
|
||||
</div>
|
||||
|
||||
<p className="py-2 text-sm text-base-content/70">
|
||||
Loaded Profile for User ID:{" "}
|
||||
<span className="badge badge-neutral font-mono font-semibold">
|
||||
{userId}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="card-actions justify-end mt-4">
|
||||
<button
|
||||
onClick={() => setIsFollowing(!isFollowing)}
|
||||
className={`btn ${
|
||||
isFollowing ? "btn-outline btn-error" : "btn-primary"
|
||||
}`}
|
||||
>
|
||||
{isFollowing ? "Unfollow" : "Follow User"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import UserProfileClient from "./UserProfileClient";
|
||||
import UseEffectBasics from "./useEffect-demo";
|
||||
import FetchGitUsers from "./FetchGitUsers";
|
||||
|
||||
export default function UserPage() {
|
||||
return (
|
||||
<main className="bg-base-200 p-8 flex flex-col items-center justify-center">
|
||||
<div className="max-w-md w-full space-y-6">
|
||||
<header className="text-center">
|
||||
<h1 className="text-3xl font-extrabold text-base-content tracking-tight">
|
||||
User Dashboard
|
||||
</h1>
|
||||
<p className="text-sm text-base-content/60 mt-1">
|
||||
Next.js Navigation
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Client Component */}
|
||||
|
||||
<UserProfileClient />
|
||||
{/* <UseEffectBasics /> */}
|
||||
{/* <FetchGitUsers /> */}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const UseEffectBasics = () => {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const handleClick = () => {
|
||||
setValue(value + 1);
|
||||
};
|
||||
|
||||
const sayHello = () => {
|
||||
console.log("hello there");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sayHello();
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// sayHello();
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// sayHello();
|
||||
// }, [value]);
|
||||
|
||||
return (
|
||||
<div className="card w-96 bg-base-100 shadow-xl border border-base-200">
|
||||
<h1>value : {value}</h1>
|
||||
<button className="btn" onClick={handleClick}>
|
||||
click me
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default UseEffectBasics;
|
||||
Reference in new issue
Block a user