Шаг 1: Подготовка проекта
Убедитесь, что у вас есть Next.js проект с файлом package.json:
Шаг 3: Загрузка на GitHub
my-app. (если проект в корне)npm run build (по умолчанию).next (по умолчанию)npm ci (по умолчанию)npm run buildhttps://my-app.vercel.appФайл: components/ControlledInput.tsx
import React from 'react';
import './ControlledInput.css';
interface ControlledInputProps {
value: string;
onChange: (newValue: string) => void;
placeholder?: string;
}
export const ControlledInput: React.FC<ControlledInputProps> = ({
value,
onChange,
placeholder,
}) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div className="controlled-input-container">
<input
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder}
className="controlled-input-field"
/>
<span className="controlled-input-hint">Текущая длина: {value.length}</span>
</div>
);
};
Файл: components/ControlledInput.css
.controlled-input-container {
display: flex;
flex-direction: column;
gap: 5px;
max-width: 300px;
}
.controlled-input-label {
font-weight: bold;
color: #333;
font-size: 14px;
}
.controlled-input-field {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.controlled-input-field:focus {
outline: none;
border-color: #0070f3;
box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.1);
}
.controlled-input-hint {
color: #666;
font-size: 12px;
margin-top: 4px;
}
Пример использования:
'use client';
import { useState } from 'react';
import { ControlledInput } from '@/components/ControlledInput';
export default function HomePage() {
const [text, setText] = useState('');
return (
<div>
<h1>Управляемый Input</h1>
<ControlledInput
value={text}
onChange={setText}
label="Введите текст:"
placeholder="Напишите что-нибудь..."
/>
</div>
);
}
Файл: components/SimpleModal.tsx
import React from 'react';
import './SimpleModal.css';
interface SimpleModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}
export const SimpleModal = ({
isOpen,
onClose,
title,
children,
}:SimpleModalProps) => {
return isOpen ? (
<div>
<div className="simple-modal-overlay" onClick={onClose} />
<div className="simple-modal-box">
<div className="simple-modal-header">
{title && <h3 className="simple-modal-title">{title}</h3>}
<button
onClick={onClose}
className="simple-modal-close-button"
>
✕
</button>
</div>
<div className="simple-modal-content">{children}</div>
</div>
</div>
) : null;
};
Файл: components/SimpleModal.css
/* Оверлей */
.simple-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
/* Модальное окно */
.simple-modal-box {
position: fixed;
top: 50%;
left: 50%;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
z-index: 1000;
}
/* Заголовок модалки */
.simple-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
.simple-modal-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
/* Кнопка закрытия */
.simple-modal-close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}
.simple-modal-close-button:hover {
background-color: #f0f0f0;
color: #333;
}
/* Содержимое модалки */
.simple-modal-content {
padding: 20px;
color: #555;
line-height: 1.6;
}
Пример использования:
'use client';
import React, { useState } from 'react';
import { SimpleModal } from './components/SimpleModal';
import './App.css';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
return (
<div>
<button onClick={openModal}>
Открыть модалку
</button>
<SimpleModal
isOpen={isModalOpen}
onClose={closeModal}
title="Пример модального окна"
>
<p>Это содержимое модального окна.</p>
<p>Вы можете закрыть его, нажав на кнопку ✕ или на оверлей.</p>
</SimpleModal>
</div>
);
}
export default App;
Файл: components/DataFetcher.tsx
'use client';
import React, { useState, useEffect } from 'react';
import './DataFetcher.css';
export const DataFetcher: React.FC = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState<boolean>(false);
const fetchData = async () => {
setLoading(true);
const response = await fetch('https://ссылка-на-ресурс');
const result = await response.json();
setData(result);
setLoading(false);
};
useEffect(() => {
fetchData();
}, []);
if (loading) {
return <div className="data-fetcher-loading">Загрузка данных...</div>;
}
if (!data) {
return <div className="data-fetcher-empty">Нет данных</div>;
}
return (
<div className="data-fetcher-container">
<h4 className="data-fetcher-title">Данные из API:</h4>
<div className="data-fetcher-content">
<p><strong>ID:</strong> {data.id}</p>
<p><strong>Заголовок:</strong> {data.title}</p>
<p><strong>Текст:</strong> {data.body}</p>
</div>
<button onClick={fetchData} className="data-fetcher-button">
Обновить данные
</button>
</div>
);
};
Файл: components/DataFetcher.css
.data-fetcher-container {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
background-color: #fafafa;
max-width: 500px;
}
.data-fetcher-title {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.data-fetcher-content {
margin-bottom: 15px;
}
.data-fetcher-content p {
margin: 8px 0;
color: #555;
font-size: 14px;
line-height: 1.5;
}
.data-fetcher-button {
padding: 8px 16px;
background-color: #0070f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
}
.data-fetcher-button:hover {
background-color: #0051cc;
}
.data-fetcher-button:active {
transform: scale(0.98);
}
.data-fetcher-loading {
padding: 20px;
text-align: center;
color: #666;
}
.data-fetcher-error {
padding: 20px;
color: #d32f2f;
background-color: #ffebee;
border-radius: 4px;
border: 1px solid #ef5350;
}
.data-fetcher-empty {
padding: 20px;
text-align: center;
color: #999;
font-style: italic;
}
Пример использования:
import { DataFetcher } from '@/components/DataFetcher';
export default function DataPage() {
return (
<div>
<h1>Загрузка данных</h1>
<DataFetcher />
</div>
);
}
pdfsapplication/pdf.Важно: Для работы react-pdf нужно настроить CORS.
http://localhost:3000 (для локальной разработки)https://vash-domain.com (для деплоя)react-pdf выдаст ошибку загрузки, так как холст (Canvas) блокирует "чужие" источники.Перейдите в SQL Editor в Supabase и выполните следующий код. Это безопаснее и точнее, чем кликать в UI.
-- Разрешаем чтение (SELECT) файлов
create policy "Documents Select Public"
on public.documents for select
to public -- Ключевое изменение: public вместо authenticated
using (true);
npm install react-pdf
components/PdfViewer.tsx)Этот файл обязан быть клиентским ('use client').
'use client';
import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
// 1. Настраиваем воркер (обязательно)
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
export default function PdfViewer({ url }: { url: string }) {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
return (
<div>
{/* 2. Загрузчик документа */}
<Document
file={url}
onLoadSuccess={({ numPages }) => setNumPages(numPages)}
>
{/* 3. Рендер страницы (текстовый слой отключен для простоты) */}
<Page
pageNumber={pageNumber}
renderTextLayer={false}
renderAnnotationLayer={false}
width={600} // Фиксированная ширина для стабильности
/>
</Document>
{/* 4. Минимальная навигация */}
<div style={{ marginTop: 10 }}>
<button disabled={pageNumber <= 1} onClick={() => setPageNumber(p => p - 1)}>
Назад
</button>
<span style={{ margin: '0 10px' }}>
{pageNumber} / {numPages}
</span>
<button disabled={pageNumber >= numPages} onClick={() => setPageNumber(p => p + 1)}>
Вперед
</button>
</div>
</div>
);
}
app/page.tsx)Здесь мы используем dynamic импорт, чтобы отключить SSR (Серверный Рендеринг), иначе приложение упадет с ошибкой window is not defined.
import dynamic from 'next/dynamic';
// 1. Динамический импорт с отключением SSR
const PdfViewer = dynamic(() => import('@/components/PdfViewer'), {
ssr: false,
loading: () => <p>Загрузка модуля...</p>,
});
export default function Page() {
// Ваша публичная ссылка из Supabase
const pdfUrl = 'https://ващ-проект.supabase.co/storage/v1/object/public/pdfs/файл.pdf';
return (
<main style={{ padding: 20 }}>
<h1>Просмотр PDF</h1>
<PdfViewer url={pdfUrl} />
</main>
);
}
| Проблема | Решение |
|---|---|
| Текст не выделяется | Убедитесь, что импортированы CSS файлы и установлено renderTextLayer={true} |
| "GlobalWorkerOptions is not defined" | Проверьте, что файл pdf-config.ts импортирован перед использованием компонента |
| Бесконечная загрузка | Если PDF защищен, настройте CORS в хранилище (S3, Supabase и т.д.) |
| Разреши отрисовка | Увеличьте scale (например, scale={1.5}) для повышения качества |