Crypto News Aggregator با استفاده از Typescript، Next.js، NewsDataHub و APIهای CoinGecko

Summarize this content to 400 words in Persian Lang
در این مقاله، میخواهیم یک برنامه ساده اما مفید برای جمعآوری اخبار ارزهای دیجیتال بسازیم که از NewsDataHub و APIهای CoinGecko استفاده میکند. هدف این مقاله یک توسعهدهنده سطح مبتدی است – اگر فکر میکنید هر بخش ارزشی به تجربه یادگیری شما نمیافزاید، میتوانید از آن صرفنظر کنید.
همچنین می توانید ببینید که کد نهایی پروژه چگونه است: https://github.com/newsdatahub/crypto-news-aggregator
می توانید ببینید که نسخه تولیدی این برنامه دقیقاً در اینجا چگونه است: https://newsdatahub.com/crypto
بیایید با ایجاد یک پروژه Next.js جدید با پشتیبانی Typescript شروع کنیم.
npx create-next-app@latest crypto-news-aggregator –typescript
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
وقتی از شما خواسته شد، انتخاب کنید:
برای ESLint بله
نه برای Tailwind CSS (ما از ماژولهای CSS استفاده خواهیم کرد)
نه برای src/ دایرکتوری
برای App Router بله
نه برای توربوپک
نه برای سفارشی کردن نام مستعار واردات (ما این را به صورت دستی تنظیم می کنیم)
cd در پوشه پروژه:
cd crypto-news-aggregator
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
راه اندازی ساختار پروژه
پس از مقداردهی اولیه، بیایید ساختار پروژه خود را ایجاد کنیم. من هدف هر دایرکتوری و فایل را توضیح خواهم داد.
mkdir -p app/components/{news-feed,price-ticker} __tests__ types
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در پایان این آموزش باید به ساختار زیر برسید.
crypto-news-aggregator/
├── __tests__/ # Test files
│ ├── Home.test.tsx
│ ├── NewsCard.test.tsx
│ └── PriceTicker.test.tsx
├── app/ # Next.js app directory
│ ├── components/ # React components
│ │ ├── news-feed/ # News-related components
│ │ │ ├── NewsCard.tsx
│ │ │ └── index.ts
│ │ └── price-ticker/ # Price ticker components
│ │ ├── PriceTicker.tsx
│ │ └── index.ts
│ ├── layout.tsx # Root layout component
│ ├── page.module.css # Styles for main page
│ └── page.tsx # Main page component
├── public/ # Static assets
├── types/ # TypeScript type definitions
│ ├── cache.ts
│ ├── crypto.ts
│ ├── env.d.ts
│ ├── index.ts
│ └── news.ts
├── .env.example # Example environment variables
├── .env.local # Environment variables (gitignored)
├── .eslintrc.json # ESLint configuration
├── .gitignore # Git ignore rules
├── eslint.config.mjs # ESLint module configuration
├── jest.config.mjs # Jest configuration
├── jest.setup.js # Jest setup file
├── next-env.d.ts # Next.js TypeScript declarations
├── next.config.js # Next.js configuration
├── package-lock.json # Locked dependency versions
├── package.json # Project dependencies
├── README.md
├── tsconfig.json # TypeScript configuration
└── types.d.ts # Global TypeScript declarations
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اما قبل از اینکه به آنجا برسیم، باید دایرکتوری پروژه را کمی تمیز کنیم و سپس چند فایل ایجاد کنیم.
فایل هایی که می توان با خیال راحت پاک کرد
app/globals.css (اگر از فایل های module.css استفاده می کنید)
همه .svg فایل ها (در /public دایرکتوری)
README.md (حذف یا بهروزرسانی، زیرا این برنامه پیشفرض از create-next-app است)
اگر favicon.ico شما در دایرکتوری برنامه است، آن را به پوشه عمومی منتقل کنید. در حالی که فاویکون می تواند در هر دو مکان کار کند، آن را به آن منتقل کنید public/ از ساختار متعارف پیروی می کند و مکان دارایی را واضح تر می کند.
تست کردن
ما باید چندین بسته آزمایشی را نصب کنیم
npm install –save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بیایید بفهمیم که هر بسته چه کاری انجام می دهد:
@testing-library/react: ابزارهایی را برای آزمایش اجزای React ارائه می دهد
@testing-library/jest-dom: همسان Jest سفارشی را اضافه می کند
jest: چارچوب اصلی تست
jest-environment-jsdom: یک محیط مرورگر را برای تست های ما شبیه سازی می کند
ایجاد کنید types.d.ts برای تست تعاریف نوع
import ‘@testing-library/jest-dom’;
declare global {
namespace jest {
interface Matchers<R> {
toBeInTheDocument(): R;
}
}
interface Window {
fetch: jest.Mock;
}
}
export {};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
حال بیایید تعاریف نوع TypeScript را نصب کنیم تا ویرایشگر کد ما بتواند Node.js، React و Jest APIها را درک کند، و امکان تکمیل خودکار و گرفتن خطاهای نوع در طول توسعه را فراهم کند.
npm install –save-dev @types/node @types/react @types/jest
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پس از نصب بسته ها، باید جست را پیکربندی کنیم. ایجاد یک jest.config.mjs فایل در ریشه پروژه خود
jest.config.mjs:
import nextJest from ‘next/jest.js’;
const createJestConfig = nextJest({
dir: ‘./’,
});
export default createJestConfig({
testEnvironment: ‘jest-environment-jsdom’,
setupFilesAfterEnv: [‘/jest.setup.js’]
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد یک jest.setup.js فایل برای وارد کردن تطبیق دهنده های DOM.
jest.setup.js:
import ‘@testing-library/jest-dom’;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در نهایت، این خطوط را اضافه کنید تا اسکریپت آزمایشی شما اجرا شود package.json زیر scripts:
“test”: “jest”,
“test:watch”: “jest –watch”
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بنابراین به این صورت خواهد بود:
“scripts”: {
“dev”: “next dev”,
“build”: “next build”,
“start”: “next start”,
“lint”: “next lint”,
“test”: “jest”,
“test:watch”: “jest –watch”
},
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اکنون می توانید با استفاده از تست ها را اجرا کنید npm test یا npm run test:watch برای حالت تماشا اما ما هنوز هیچ تستی نداریم، به زودی یک تست اضافه خواهیم کرد.
دریافت نشانه API NewsDataHub شما
بیایید روند دریافت توکن API شما را طی کنیم.
به NewsDataHub.com مراجعه کنید
ایجاد حساب کاربری (بدون نیاز به کارت اعتباری)
آدرس ایمیل خود را در فرم ثبت نام وارد کنید
باید ایمیل خود را برای کد تایید بررسی کنید
هنگامی که حساب خود را تأیید کردید، به داشبورد خود منتقل خواهید شد، جایی که می توانید کلید API خود را پیدا کنید
افزودن کلید API به پروژه شما
ایجاد یک .env.example در ریشه پروژه خود فایل کنید تا به عنوان یک الگو برای متغیرهای محیط مورد نیاز عمل کند
NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1/news
NEXT_PUBLIC_API_TOKEN=your_token_here
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
سپس برای کپی موارد زیر را اجرا کنید .env.example قالب به .env.local که در آن پیکربندی واقعی شما خواهد بود
cp .env.example .env.local
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
جایگزین کنید your_token_here در .env.local با توکن NewsDataHub API خود از داشبورد خود.
.env.example به git به عنوان یک الگو متعهد است، while .env.local حاوی اسرار واقعی است و نادیده گرفته می شود.
هنگام ارجاع به کلید API از کلمات “token” و “key” به جای یکدیگر استفاده می کنیم. هنگامی که توکن API خود را دارید، آن را به پروژه خود اضافه کنید .env.local فایل:
NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1
NEXT_PUBLIC_API_TOKEN=your_newsdatahub_token
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تنظیمات و نمای کلی فایل های پیکربندی
بیایید فایل های پیکربندی ضروری را تنظیم کنیم. من هدف و محتوای هر یک را توضیح می دهم:
به روز رسانی .gitignore
این .gitignore فایل به طور خودکار در طول اولیه سازی پروژه ایجاد شد. به Git می گوید کدام فایل ها و پوشه ها را از کنترل نسخه حذف کند.
بیایید از خودمان مطمئن شویم .env.local فایل با افزودن موارد زیر به آن نادیده گرفته می شود .gitignore.
# Environment files
.env*.local
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تنظیم تعاریف نوع محیط
ایجاد کنید types/env.d.ts برای ارائه تعاریف نوع TypeScript برای متغیرهای محیطی:
declare global {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_API_TOKEN: string;
}
}
}
export {};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این فایل به TypeScript در مورد متغیرهای محیطی ما می گوید و امکان بررسی صحیح نوع را در هنگام دسترسی فراهم می کند process.env مقادیر و ارائه پیشنهادات تکمیل خودکار. بدون آن، TypeScript این متغیرها را از نوع خود در نظر می گیرد any .
راه اندازی ESLint
اضافه کنید .eslintrc.json برای فعال کردن قوانین پیشفرض Next.js برای عملکرد و بهترین شیوهها.
{
“extends”: [
“next/core-web-vitals”
]
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
حالا بیایید کد پروژه را اضافه کنیم
ایجاد کنید types/cache.ts.ساختار سیستم حافظه پنهان سمت سرویس گیرنده ما را تعریف می کند و نحوه ذخیره مهرهای زمانی و داده های خبری را مشخص می کند.
import { NewsItem } from “.”;
export interface CacheData {
timestamp: number;
data: NewsItem[];
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد کنید types/crypto.ts.ساختار داده قیمت ارزهای دیجیتال را از طریق CoinCap API، از جمله قیمت، ارزش بازار و تغییرات 24 ساعته تعریف میکند.
export type CoinData = {
[key: string]: {
usd: number;
usd_market_cap: number;
usd_24h_vol: number;
usd_24h_change: number;
last_updated_at: number;
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد کنید news.tsحاوی رابطهایی برای موارد خبری از NewsDataHub API و لوازم جانبی برای مؤلفه NewsCard ما.
export interface NewsItem {
id: string;
title: string;
article_link: string;
description: string;
pub_date: string;
}
export interface NewsCardProps {
index: number;
item: NewsItem;
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد کنید types/index.ts.نقطه صادرات مرکزی برای همه تعاریف نوع، امکان واردات پاک را فراهم می کند.
export * from ‘./cache’;
export * from ‘./news’;
export * from ‘./crypto’;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پیاده سازی کامپوننت NewsCard و PriceTicker
در مرحله بعد، ما اجزای خود و سبک آنها را پیاده سازی می کنیم. هر جزء باید در دایرکتوری مربوطه خود باشد.
جزء NewsCard
app/components/news-feed/NewsCard.tsx
import styles from ‘./styles.module.css’;
import { NewsCardProps } from ‘@/types’;
export const NewsCard: React.FC<NewsCardProps> = ({index, item}) => {
return (
<div key={index} className={styles.newsCard}>
<h2 className={styles.newsTitle}>{item.title}h2>
<p>{item.description.slice(0, 200)+”…”} Read more
<br/>
<br/>
<a href={item.article_link} target=”_blank” rel=”noopener noreferrer” className={styles.newsLink}>
{item.article_link}
a>
p>
<div className={styles.newsDate}>
{new Date(item.pub_date).toLocaleDateString()}
div>
div>
)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
app/components/news-feed/styles.module.css
.newsCard {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.newsTitle {
margin: 0 0 10px 0;
font-size: 1.2em;
color: #2e009a;
font-family: math;
}
.newsDate {
color: #666;
font-size: 0.9em;
margin-top: 10px;
}
.newsLink {
color:darkcyan;
}
.newsLink:hover {
color: rgb(3, 79, 79);
cursor: pointer;
text-decoration: underline;
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
app/components/news-feed/index.tsx
export { NewsCard } from ‘./NewsCard’;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
کامپوننت PriceTicker
app/components/price-ticker/PriceTicker.tsx
import { useState, useEffect } from ‘react’;
import styles from ‘./styles.module.css’;
import { CoinData } from ‘@/types’;
export const PriceTicker = () => {
const [prices, setPrices] = useState<CoinData>({});
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const fetchPrices = async () => {
try {
const response = await fetch(‘https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,dogecoin&vs_currencies=usd&include_24hr_change=true’);
if (!response.ok) throw new Error(‘Failed to fetch prices’);
const data = await response.json();
setPrices(data);
setError(null);
} catch (err) {
setError(‘Failed to load prices’);
console.error(‘Price fetch error:’, err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPrices();
const interval = setInterval(fetchPrices, 60000); // Update every minute
return () => clearInterval(interval);
}, []);
if (loading) return <div className={styles.ticker}>Loading prices…div>;
if (error) return <div className={styles.ticker}>Price data unavailablediv>;
return (
<div className={styles.ticker}>
{Object.entries(prices).map(([coinId, data]) => {
const price = data.usd.toLocaleString(‘en-US’, {
style: ‘currency’,
currency: ‘USD’,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const change = data.usd_24h_change || 0;
const changeClass = change >= 0 ? styles.positive : styles.negative;
return (
<div key={coinId} className={styles.cryptoPrice}>
<span className={styles.symbol}>{coinId.toUpperCase()}span>
<span className={styles.price}>{price}span>
<span className={`${styles.change} ${changeClass}`}>
{change >= 0 ? ‘↑’ : ‘↓’}
{Math.abs(change).toFixed(2)}%
span>
div>
);
})}
div>
);
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
app/components/price-ticker/styles.module.css
.ticker {
background: #1a1a1a;
color: white;
padding: 10px;
border-radius: 8px;
margin-bottom: 20px;
overflow-x: auto;
display: flex;
gap: 20px;
align-items: center;
}
.cryptoPrice {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
white-space: nowrap;
}
.symbol {
font-weight: bold;
color: #ffd700;
}
.price {
font-family: monospace;
}
.change {
font-size: 0.9em;
padding: 2px 6px;
border-radius: 4px;
}
.positive {
color: #00ff00;
}
.negative {
color: #ff4444;
}
@keyframes slide {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
app/components/news-feed/index.tsx
export { PriceTicker } from ‘./PriceTicker’;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ساخت مؤلفه صفحه اصلی
در Next.js App Router، صفحه اصلی برنامه ما در آن قرار دارد app/page.tsx. در حالی که فایل قرار است نامگذاری شود page.tsx به دنبال قراردادهای Next.js، کامپوننت خود را نامگذاری می کنیم Home تا به وضوح هدف آن را به عنوان صفحه اصلی برنامه ما نشان دهد.
پیش بروید و به روز کنید app/components/page.tsx با کد زیر
‘use client’;
import { useState, useEffect } from ‘react’;
import { PriceTicker } from ‘@/app/components/price-ticker’;
import { NewsCard } from ‘@/app/components/news-feed’;
import { CacheData, NewsItem } from ‘@/types’;
import styles from ‘./page.module.css’;
// Environment variables for API configuration
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;
// Cache duration set to one hour
const CACHE_DURATION = 1000 * 60 * 60;
const TOPICS = [‘cryptocurrency’];
// In-memory cache for storing news data
const cache: Record<string, CacheData> = {};
export default function Home() {
// State management using React hooks
const [news, setNews] = useState<NewsItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
// Fetches news data with built-in caching
const fetchNews = async (topics: string[]) => {
const cacheKey = topics.sort().join(‘,’);
const cachedData = cache[cacheKey];
if (cachedData && Date.now() – cachedData.timestamp < CACHE_DURATION) {
setNews(cachedData.data);
setLastUpdated(new Date(cachedData.timestamp));
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}?language=en&topic=cryptocurrency`, {
headers: {
‘x-api-key’: API_TOKEN,
‘Content-Type’: ‘application/json’
},
});
if (!response.ok) throw new Error(‘Failed to fetch news’);
const articles = await response.json();
const data: NewsItem[] = articles.data;
cache[cacheKey] = {
timestamp: Date.now(),
data
};
setNews(data);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : ‘An error occurred’);
} finally {
setLoading(false);
}
};
// Fetch data when component mounts
useEffect(() => {
fetchNews(TOPICS);
}, []);
// Handler for manual refresh
const handleRefresh = () => {
const cacheKey = TOPICS.sort().join(‘,’);
delete cache[cacheKey];
fetchNews(TOPICS);
};
return (
<div className={styles.container}>
<PriceTicker />
{lastUpdated && (
<div className={styles.lastUpdated}>
Last updated: {lastUpdated.toLocaleTimeString()}
<button onClick={handleRefresh} className={styles.refreshButton}>
Refresh
button>
div>
)}
{error && <div className={styles.error}>{error}div>}
{loading ? (
<div className={styles.loading}>Loading…div>
) : (
news.map((item: NewsItem, index: number) => (
<NewsCard key={item.id || index} item={item} index={index} />
))
)}
div>
);
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
به روز رسانی page.module.css با سبک های زیر
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.topics {
margin-bottom: 20px;
}
.topic {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: none;
cursor: pointer;
}
.topicSelected {
background: #007bff;
color: white;
border-color: #007bff;
}
.error {
color: #dc3545;
padding: 10px;
border: 1px solid #dc3545;
border-radius: 4px;
margin-bottom: 15px;
}
.loading {
text-align: center;
padding: 20px;
}
.lastUpdated {
color: #666;
font-size: 0.9em;
margin-bottom: 15px;
}
.refreshButton {
background: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
به روز رسانی app/layout.tsx با موارد زیر:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang=”en”>
<head>
<title>Crypto News Aggregator Applicationtitle>
<meta name=”description” content=”Crypto News Aggregator Application” />
head>
<body>
{children}
body>
html>
)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اجرای پروژه شما
ادامه دهید و پروژه خود را اجرا کنید
npm run dev
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
می توانید برنامه در حال اجرا خود را در https://localhost:3000 پیدا کنید
بابت اتمام پروژه تبریک می گویم! 🏆 👏
تست کامپوننت صفحه
ما میخواهیم آزمایشی برای مؤلفه Home اضافه کنیم که تأیید میکند که محتوای اخبار پس از واکشی دادهها به درستی ارائه میشود. ادامه دهید و این فایل آزمایشی را ایجاد کنید.
__tests__/Home.test.tsx
import { render, screen, waitFor } from ‘@testing-library/react’;
import Home from ‘@/app/page’;
describe(‘Home’, () => {
beforeEach(() => {
// Set up test environment variables
process.env.NEXT_PUBLIC_API_URL = ‘http://test-api.com’;
process.env.NEXT_PUBLIC_API_TOKEN = ‘test-token’;
// Mock fetch for both API endpoints
global.fetch = jest.fn((url) => {
// Mock responses for different API calls
if (url.includes(‘api.coingecko.com’)) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
bitcoin: { usd: 65000, usd_24h_change: 2.5 }
}),
status: 200,
} as Response);
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
data: [{
id: ‘1’,
title: ‘News Title’,
description: ‘News Description’,
url: ‘https://test.com’,
published_at: ‘2024-03-25’
}]
}),
status: 200,
} as Response);
}) as jest.Mock;
});
test(‘renders news feed’, async () => {
render(<Home />);
await waitFor(
() => expect(screen.getByText(“News Title”)).toBeInTheDocument(),
{ timeout: 3000 }
);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست را اجرا کنید
npm run test
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این آزمایش تأیید میکند که مؤلفه Home ما با موفقیت محتوای اخبار را پس از واکشی دادهها ارائه میکند.
تست های اضافی برای اجزای PriceTicker و NewsCard را می توان در مخزن GitHub پروژه یافت. این تستها عملکرد و رفتار رندرینگ را پوشش میدهند. من شما را تشویق می کنم که تست های بیشتری برای این پروژه ایجاد کنید.
بهبود بیشتر این پروژه را در نظر بگیرید.
چند ایده:
اجرای حالت های بارگذاری مناسب
صفحه بندی برای اخبار اضافه کنید
ذخیره سازی پیچیده تری را پیاده سازی کنید
مجموعه آزمایشی را تقویت کنید
می توانید پارامتر پرس و جو موضوع را برای واکشی انواع مختلف اخبار تغییر دهید
از اینکه دنبال می کنید متشکرم 😄
اعتبار تصویر جلد: عکس توسط پروژه سهام RDNE: https://www.pexels.com/photo/selective-focus-photo-of-silver-and-gold-bitcoins-8369648/
در این مقاله، میخواهیم یک برنامه ساده اما مفید برای جمعآوری اخبار ارزهای دیجیتال بسازیم که از NewsDataHub و APIهای CoinGecko استفاده میکند. هدف این مقاله یک توسعهدهنده سطح مبتدی است – اگر فکر میکنید هر بخش ارزشی به تجربه یادگیری شما نمیافزاید، میتوانید از آن صرفنظر کنید.
همچنین می توانید ببینید که کد نهایی پروژه چگونه است: https://github.com/newsdatahub/crypto-news-aggregator
می توانید ببینید که نسخه تولیدی این برنامه دقیقاً در اینجا چگونه است: https://newsdatahub.com/crypto
بیایید با ایجاد یک پروژه Next.js جدید با پشتیبانی Typescript شروع کنیم.
npx create-next-app@latest crypto-news-aggregator --typescript
وقتی از شما خواسته شد، انتخاب کنید:
- برای ESLint بله
- نه برای Tailwind CSS (ما از ماژولهای CSS استفاده خواهیم کرد)
- نه برای
src/
دایرکتوری - برای App Router بله
- نه برای توربوپک
- نه برای سفارشی کردن نام مستعار واردات (ما این را به صورت دستی تنظیم می کنیم)
cd در پوشه پروژه:
cd crypto-news-aggregator
راه اندازی ساختار پروژه
پس از مقداردهی اولیه، بیایید ساختار پروژه خود را ایجاد کنیم. من هدف هر دایرکتوری و فایل را توضیح خواهم داد.
mkdir -p app/components/{news-feed,price-ticker} __tests__ types
در پایان این آموزش باید به ساختار زیر برسید.
crypto-news-aggregator/
├── __tests__/ # Test files
│ ├── Home.test.tsx
│ ├── NewsCard.test.tsx
│ └── PriceTicker.test.tsx
├── app/ # Next.js app directory
│ ├── components/ # React components
│ │ ├── news-feed/ # News-related components
│ │ │ ├── NewsCard.tsx
│ │ │ └── index.ts
│ │ └── price-ticker/ # Price ticker components
│ │ ├── PriceTicker.tsx
│ │ └── index.ts
│ ├── layout.tsx # Root layout component
│ ├── page.module.css # Styles for main page
│ └── page.tsx # Main page component
├── public/ # Static assets
├── types/ # TypeScript type definitions
│ ├── cache.ts
│ ├── crypto.ts
│ ├── env.d.ts
│ ├── index.ts
│ └── news.ts
├── .env.example # Example environment variables
├── .env.local # Environment variables (gitignored)
├── .eslintrc.json # ESLint configuration
├── .gitignore # Git ignore rules
├── eslint.config.mjs # ESLint module configuration
├── jest.config.mjs # Jest configuration
├── jest.setup.js # Jest setup file
├── next-env.d.ts # Next.js TypeScript declarations
├── next.config.js # Next.js configuration
├── package-lock.json # Locked dependency versions
├── package.json # Project dependencies
├── README.md
├── tsconfig.json # TypeScript configuration
└── types.d.ts # Global TypeScript declarations
اما قبل از اینکه به آنجا برسیم، باید دایرکتوری پروژه را کمی تمیز کنیم و سپس چند فایل ایجاد کنیم.
فایل هایی که می توان با خیال راحت پاک کرد
- app/globals.css (اگر از فایل های module.css استفاده می کنید)
- همه
.svg
فایل ها (در/public
دایرکتوری) - README.md (حذف یا بهروزرسانی، زیرا این برنامه پیشفرض از create-next-app است)
اگر favicon.ico شما در دایرکتوری برنامه است، آن را به پوشه عمومی منتقل کنید. در حالی که فاویکون می تواند در هر دو مکان کار کند، آن را به آن منتقل کنید public/
از ساختار متعارف پیروی می کند و مکان دارایی را واضح تر می کند.
تست کردن
ما باید چندین بسته آزمایشی را نصب کنیم
npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
بیایید بفهمیم که هر بسته چه کاری انجام می دهد:
-
@testing-library/react
: ابزارهایی را برای آزمایش اجزای React ارائه می دهد -
@testing-library/jest-dom
: همسان Jest سفارشی را اضافه می کند -
jest
: چارچوب اصلی تست -
jest-environment-jsdom
: یک محیط مرورگر را برای تست های ما شبیه سازی می کند
ایجاد کنید types.d.ts
برای تست تعاریف نوع
import '@testing-library/jest-dom';
declare global {
namespace jest {
interface Matchers<R> {
toBeInTheDocument(): R;
}
}
interface Window {
fetch: jest.Mock;
}
}
export {};
حال بیایید تعاریف نوع TypeScript را نصب کنیم تا ویرایشگر کد ما بتواند Node.js، React و Jest APIها را درک کند، و امکان تکمیل خودکار و گرفتن خطاهای نوع در طول توسعه را فراهم کند.
npm install --save-dev @types/node @types/react @types/jest
پس از نصب بسته ها، باید جست را پیکربندی کنیم. ایجاد یک jest.config.mjs
فایل در ریشه پروژه خود
jest.config.mjs
:
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
dir: './',
});
export default createJestConfig({
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['/jest.setup.js ']
});
ایجاد یک jest.setup.js
فایل برای وارد کردن تطبیق دهنده های DOM.
jest.setup.js
:
import '@testing-library/jest-dom';
در نهایت، این خطوط را اضافه کنید تا اسکریپت آزمایشی شما اجرا شود package.json
زیر scripts
:
"test": "jest",
"test:watch": "jest --watch"
بنابراین به این صورت خواهد بود:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:watch": "jest --watch"
},
اکنون می توانید با استفاده از تست ها را اجرا کنید npm test
یا npm run test:watch
برای حالت تماشا اما ما هنوز هیچ تستی نداریم، به زودی یک تست اضافه خواهیم کرد.
دریافت نشانه API NewsDataHub شما
بیایید روند دریافت توکن API شما را طی کنیم.
به NewsDataHub.com مراجعه کنید
ایجاد حساب کاربری (بدون نیاز به کارت اعتباری)
- آدرس ایمیل خود را در فرم ثبت نام وارد کنید
- باید ایمیل خود را برای کد تایید بررسی کنید
- هنگامی که حساب خود را تأیید کردید، به داشبورد خود منتقل خواهید شد، جایی که می توانید کلید API خود را پیدا کنید
افزودن کلید API به پروژه شما
ایجاد یک .env.example
در ریشه پروژه خود فایل کنید تا به عنوان یک الگو برای متغیرهای محیط مورد نیاز عمل کند
NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1/news
NEXT_PUBLIC_API_TOKEN=your_token_here
سپس برای کپی موارد زیر را اجرا کنید .env.example
قالب به .env.local
که در آن پیکربندی واقعی شما خواهد بود
cp .env.example .env.local
جایگزین کنید your_token_here
در .env.local
با توکن NewsDataHub API خود از داشبورد خود.
.env.example
به git به عنوان یک الگو متعهد است، while .env.local
حاوی اسرار واقعی است و نادیده گرفته می شود.
هنگام ارجاع به کلید API از کلمات “token” و “key” به جای یکدیگر استفاده می کنیم. هنگامی که توکن API خود را دارید، آن را به پروژه خود اضافه کنید .env.local
فایل:
NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1
NEXT_PUBLIC_API_TOKEN=your_newsdatahub_token
تنظیمات و نمای کلی فایل های پیکربندی
بیایید فایل های پیکربندی ضروری را تنظیم کنیم. من هدف و محتوای هر یک را توضیح می دهم:
به روز رسانی .gitignore
این .gitignore
فایل به طور خودکار در طول اولیه سازی پروژه ایجاد شد. به Git می گوید کدام فایل ها و پوشه ها را از کنترل نسخه حذف کند.
بیایید از خودمان مطمئن شویم .env.local
فایل با افزودن موارد زیر به آن نادیده گرفته می شود .gitignore
.
# Environment files
.env*.local
تنظیم تعاریف نوع محیط
ایجاد کنید types/env.d.ts
برای ارائه تعاریف نوع TypeScript برای متغیرهای محیطی:
declare global {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_API_TOKEN: string;
}
}
}
export {};
این فایل به TypeScript در مورد متغیرهای محیطی ما می گوید و امکان بررسی صحیح نوع را در هنگام دسترسی فراهم می کند process.env
مقادیر و ارائه پیشنهادات تکمیل خودکار. بدون آن، TypeScript این متغیرها را از نوع خود در نظر می گیرد any
.
راه اندازی ESLint
اضافه کنید .eslintrc.json
برای فعال کردن قوانین پیشفرض Next.js برای عملکرد و بهترین شیوهها.
{
"extends": [
"next/core-web-vitals"
]
}
حالا بیایید کد پروژه را اضافه کنیم
ایجاد کنید types/cache.ts
.
ساختار سیستم حافظه پنهان سمت سرویس گیرنده ما را تعریف می کند و نحوه ذخیره مهرهای زمانی و داده های خبری را مشخص می کند.
import { NewsItem } from ".";
export interface CacheData {
timestamp: number;
data: NewsItem[];
}
ایجاد کنید types/crypto.ts
.
ساختار داده قیمت ارزهای دیجیتال را از طریق CoinCap API، از جمله قیمت، ارزش بازار و تغییرات 24 ساعته تعریف میکند.
export type CoinData = {
[key: string]: {
usd: number;
usd_market_cap: number;
usd_24h_vol: number;
usd_24h_change: number;
last_updated_at: number;
}
}
ایجاد کنید news.ts
حاوی رابطهایی برای موارد خبری از NewsDataHub API و لوازم جانبی برای مؤلفه NewsCard ما.
export interface NewsItem {
id: string;
title: string;
article_link: string;
description: string;
pub_date: string;
}
export interface NewsCardProps {
index: number;
item: NewsItem;
}
ایجاد کنید types/index.ts
.
نقطه صادرات مرکزی برای همه تعاریف نوع، امکان واردات پاک را فراهم می کند.
export * from './cache';
export * from './news';
export * from './crypto';
پیاده سازی کامپوننت NewsCard و PriceTicker
در مرحله بعد، ما اجزای خود و سبک آنها را پیاده سازی می کنیم. هر جزء باید در دایرکتوری مربوطه خود باشد.
جزء NewsCard
app/components/news-feed/NewsCard.tsx
import styles from './styles.module.css';
import { NewsCardProps } from '@/types';
export const NewsCard: React.FC<NewsCardProps> = ({index, item}) => {
return (
<div key={index} className={styles.newsCard}>
<h2 className={styles.newsTitle}>{item.title}h2>
<p>{item.description.slice(0, 200)+"..."} Read more
<br/>
<br/>
<a href={item.article_link} target="_blank" rel="noopener noreferrer" className={styles.newsLink}>
{item.article_link}
a>
p>
<div className={styles.newsDate}>
{new Date(item.pub_date).toLocaleDateString()}
div>
div>
)
}
app/components/news-feed/styles.module.css
.newsCard {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.newsTitle {
margin: 0 0 10px 0;
font-size: 1.2em;
color: #2e009a;
font-family: math;
}
.newsDate {
color: #666;
font-size: 0.9em;
margin-top: 10px;
}
.newsLink {
color:darkcyan;
}
.newsLink:hover {
color: rgb(3, 79, 79);
cursor: pointer;
text-decoration: underline;
}
app/components/news-feed/index.tsx
export { NewsCard } from './NewsCard';
کامپوننت PriceTicker
app/components/price-ticker/PriceTicker.tsx
import { useState, useEffect } from 'react';
import styles from './styles.module.css';
import { CoinData } from '@/types';
export const PriceTicker = () => {
const [prices, setPrices] = useState<CoinData>({});
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const fetchPrices = async () => {
try {
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,dogecoin&vs_currencies=usd&include_24hr_change=true');
if (!response.ok) throw new Error('Failed to fetch prices');
const data = await response.json();
setPrices(data);
setError(null);
} catch (err) {
setError('Failed to load prices');
console.error('Price fetch error:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPrices();
const interval = setInterval(fetchPrices, 60000); // Update every minute
return () => clearInterval(interval);
}, []);
if (loading) return <div className={styles.ticker}>Loading prices...div>;
if (error) return <div className={styles.ticker}>Price data unavailablediv>;
return (
<div className={styles.ticker}>
{Object.entries(prices).map(([coinId, data]) => {
const price = data.usd.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const change = data.usd_24h_change || 0;
const changeClass = change >= 0 ? styles.positive : styles.negative;
return (
<div key={coinId} className={styles.cryptoPrice}>
<span className={styles.symbol}>{coinId.toUpperCase()}span>
<span className={styles.price}>{price}span>
<span className={`${styles.change} ${changeClass}`}>
{change >= 0 ? '↑' : '↓'}
{Math.abs(change).toFixed(2)}%
span>
div>
);
})}
div>
);
}
app/components/price-ticker/styles.module.css
.ticker {
background: #1a1a1a;
color: white;
padding: 10px;
border-radius: 8px;
margin-bottom: 20px;
overflow-x: auto;
display: flex;
gap: 20px;
align-items: center;
}
.cryptoPrice {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
white-space: nowrap;
}
.symbol {
font-weight: bold;
color: #ffd700;
}
.price {
font-family: monospace;
}
.change {
font-size: 0.9em;
padding: 2px 6px;
border-radius: 4px;
}
.positive {
color: #00ff00;
}
.negative {
color: #ff4444;
}
@keyframes slide {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
app/components/news-feed/index.tsx
export { PriceTicker } from './PriceTicker';
ساخت مؤلفه صفحه اصلی
در Next.js App Router، صفحه اصلی برنامه ما در آن قرار دارد app/page.tsx
. در حالی که فایل قرار است نامگذاری شود page.tsx
به دنبال قراردادهای Next.js، کامپوننت خود را نامگذاری می کنیم Home
تا به وضوح هدف آن را به عنوان صفحه اصلی برنامه ما نشان دهد.
پیش بروید و به روز کنید app/components/page.tsx
با کد زیر
'use client';
import { useState, useEffect } from 'react';
import { PriceTicker } from '@/app/components/price-ticker';
import { NewsCard } from '@/app/components/news-feed';
import { CacheData, NewsItem } from '@/types';
import styles from './page.module.css';
// Environment variables for API configuration
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;
// Cache duration set to one hour
const CACHE_DURATION = 1000 * 60 * 60;
const TOPICS = ['cryptocurrency'];
// In-memory cache for storing news data
const cache: Record<string, CacheData> = {};
export default function Home() {
// State management using React hooks
const [news, setNews] = useState<NewsItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
// Fetches news data with built-in caching
const fetchNews = async (topics: string[]) => {
const cacheKey = topics.sort().join(',');
const cachedData = cache[cacheKey];
if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) {
setNews(cachedData.data);
setLastUpdated(new Date(cachedData.timestamp));
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}?language=en&topic=cryptocurrency`, {
headers: {
'x-api-key': API_TOKEN,
'Content-Type': 'application/json'
},
});
if (!response.ok) throw new Error('Failed to fetch news');
const articles = await response.json();
const data: NewsItem[] = articles.data;
cache[cacheKey] = {
timestamp: Date.now(),
data
};
setNews(data);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
// Fetch data when component mounts
useEffect(() => {
fetchNews(TOPICS);
}, []);
// Handler for manual refresh
const handleRefresh = () => {
const cacheKey = TOPICS.sort().join(',');
delete cache[cacheKey];
fetchNews(TOPICS);
};
return (
<div className={styles.container}>
<PriceTicker />
{lastUpdated && (
<div className={styles.lastUpdated}>
Last updated: {lastUpdated.toLocaleTimeString()}
<button onClick={handleRefresh} className={styles.refreshButton}>
Refresh
button>
div>
)}
{error && <div className={styles.error}>{error}div>}
{loading ? (
<div className={styles.loading}>Loading...div>
) : (
news.map((item: NewsItem, index: number) => (
<NewsCard key={item.id || index} item={item} index={index} />
))
)}
div>
);
}
به روز رسانی page.module.css
با سبک های زیر
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.topics {
margin-bottom: 20px;
}
.topic {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: none;
cursor: pointer;
}
.topicSelected {
background: #007bff;
color: white;
border-color: #007bff;
}
.error {
color: #dc3545;
padding: 10px;
border: 1px solid #dc3545;
border-radius: 4px;
margin-bottom: 15px;
}
.loading {
text-align: center;
padding: 20px;
}
.lastUpdated {
color: #666;
font-size: 0.9em;
margin-bottom: 15px;
}
.refreshButton {
background: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
به روز رسانی app/layout.tsx
با موارد زیر:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<title>Crypto News Aggregator Applicationtitle>
<meta name="description" content="Crypto News Aggregator Application" />
head>
<body>
{children}
body>
html>
)
}
اجرای پروژه شما
ادامه دهید و پروژه خود را اجرا کنید
npm run dev
می توانید برنامه در حال اجرا خود را در https://localhost:3000 پیدا کنید
بابت اتمام پروژه تبریک می گویم! 🏆 👏
تست کامپوننت صفحه
ما میخواهیم آزمایشی برای مؤلفه Home اضافه کنیم که تأیید میکند که محتوای اخبار پس از واکشی دادهها به درستی ارائه میشود. ادامه دهید و این فایل آزمایشی را ایجاد کنید.
__tests__/Home.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import Home from '@/app/page';
describe('Home', () => {
beforeEach(() => {
// Set up test environment variables
process.env.NEXT_PUBLIC_API_URL = 'http://test-api.com';
process.env.NEXT_PUBLIC_API_TOKEN = 'test-token';
// Mock fetch for both API endpoints
global.fetch = jest.fn((url) => {
// Mock responses for different API calls
if (url.includes('api.coingecko.com')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
bitcoin: { usd: 65000, usd_24h_change: 2.5 }
}),
status: 200,
} as Response);
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
data: [{
id: '1',
title: 'News Title',
description: 'News Description',
url: 'https://test.com',
published_at: '2024-03-25'
}]
}),
status: 200,
} as Response);
}) as jest.Mock;
});
test('renders news feed', async () => {
render(<Home />);
await waitFor(
() => expect(screen.getByText("News Title")).toBeInTheDocument(),
{ timeout: 3000 }
);
});
});
تست را اجرا کنید
npm run test
این آزمایش تأیید میکند که مؤلفه Home ما با موفقیت محتوای اخبار را پس از واکشی دادهها ارائه میکند.
تست های اضافی برای اجزای PriceTicker و NewsCard را می توان در مخزن GitHub پروژه یافت. این تستها عملکرد و رفتار رندرینگ را پوشش میدهند. من شما را تشویق می کنم که تست های بیشتری برای این پروژه ایجاد کنید.
بهبود بیشتر این پروژه را در نظر بگیرید.
چند ایده:
- اجرای حالت های بارگذاری مناسب
- صفحه بندی برای اخبار اضافه کنید
- ذخیره سازی پیچیده تری را پیاده سازی کنید
- مجموعه آزمایشی را تقویت کنید
- می توانید پارامتر پرس و جو موضوع را برای واکشی انواع مختلف اخبار تغییر دهید
از اینکه دنبال می کنید متشکرم 😄
اعتبار تصویر جلد: عکس توسط پروژه سهام RDNE: https://www.pexels.com/photo/selective-focus-photo-of-silver-and-gold-bitcoins-8369648/