ADD:增加AIdemo
This commit is contained in:
parent
ad0db44e3c
commit
5eca37cfdc
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "llm-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,824 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
function App() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [files, setFiles] = useState([]);
|
||||
const [model, setModel] = useState('bailian-token-plan/qwen3.6-plus');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [previewContent, setPreviewContent] = useState('');
|
||||
const [thinking, setThinking] = useState(false);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [generatedFiles, setGeneratedFiles] = useState([]);
|
||||
const [activePreview, setActivePreview] = useState(null);
|
||||
const [previewMode, setPreviewMode] = useState('code'); // 'code' 或 'preview'
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [currentSession, setCurrentSession] = useState(null);
|
||||
const [sessionInput, setSessionInput] = useState('');
|
||||
const [showSessionModal, setShowSessionModal] = useState(false);
|
||||
const [sessionMessagesCache, setSessionMessagesCache] = useState({}); // 会话消息缓存
|
||||
const chatRef = useRef(null);
|
||||
|
||||
const modelOptions = [
|
||||
{ value: 'bailian-token-plan/qwen3.6-plus', label: 'Qwen3.6 Plus (百炼)' },
|
||||
{ value: 'bailian-token-plan/MiniMax-M2.5', label: 'MiniMax M2.5 (百炼)' },
|
||||
{ value: 'bailian-token-plan/glm-5', label: 'GLM-5 (百炼)' },
|
||||
{ value: 'bailian-token-plan/deepseek-v3.2', label: 'DeepSeek V3.2 (百炼)' },
|
||||
{ value: 'ollama/qwen3-coder-next:latest', label: 'Qwen3 Coder (Ollama)' }
|
||||
];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (chatRef.current) {
|
||||
chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会话列表
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/sessions');
|
||||
setSessions(response.data.sessions);
|
||||
} catch (err) {
|
||||
console.error('加载会话列表失败:', err);
|
||||
setError('加载会话列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新会话
|
||||
const createSession = async (title) => {
|
||||
try {
|
||||
const response = await axios.post('/api/sessions', { title });
|
||||
const newSession = response.data;
|
||||
setSessions(prev => [newSession, ...prev]);
|
||||
setCurrentSession(newSession);
|
||||
setMessages([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是基于 OpenCode 的大模型助手。请问有什么我可以帮助你的吗?你可以:\n1. 直接输入问题进行对话\n2. 上传文件进行分析\n3. 切换不同的模型\n\n例如,你可以问我:"如何使用 Python 实现快速排序?" 或者上传一个 PDF 文件让我分析其中的内容。',
|
||||
generatedFiles: []
|
||||
}
|
||||
]);
|
||||
setGeneratedFiles([]);
|
||||
setShowSessionModal(false);
|
||||
setSessionInput('');
|
||||
} catch (err) {
|
||||
console.error('创建会话失败:', err);
|
||||
setError('创建会话失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 切换会话
|
||||
const switchSession = async (session) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setCurrentSession(session);
|
||||
|
||||
// 检查缓存中是否有该会话的消息
|
||||
if (sessionMessagesCache[session.id]) {
|
||||
setMessages(sessionMessagesCache[session.id]);
|
||||
} else {
|
||||
// 从服务器加载会话消息
|
||||
const response = await axios.get(`/api/sessions/${session.id}/messages`);
|
||||
const sessionMessages = response.data.messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
generatedFiles: extractGeneratedFiles(msg.content)
|
||||
}));
|
||||
const messagesToSet = sessionMessages.length > 0 ? sessionMessages : [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是基于 OpenCode 的大模型助手。请问有什么我可以帮助你的吗?你可以:\n1. 直接输入问题进行对话\n2. 上传文件进行分析\n3. 切换不同的模型\n\n例如,你可以问我:"如何使用 Python 实现快速排序?" 或者上传一个 PDF 文件让我分析其中的内容。',
|
||||
generatedFiles: []
|
||||
}
|
||||
];
|
||||
setMessages(messagesToSet);
|
||||
// 将消息缓存起来
|
||||
setSessionMessagesCache(prev => ({
|
||||
...prev,
|
||||
[session.id]: messagesToSet
|
||||
}));
|
||||
}
|
||||
setGeneratedFiles([]);
|
||||
} catch (err) {
|
||||
console.error('切换会话失败:', err);
|
||||
setError('切换会话失败');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除会话
|
||||
const deleteSession = async (sessionId) => {
|
||||
try {
|
||||
await axios.delete(`/api/sessions/${sessionId}`);
|
||||
setSessions(prev => prev.filter(session => session.id !== sessionId));
|
||||
if (currentSession && currentSession.id === sessionId) {
|
||||
setCurrentSession(null);
|
||||
setMessages([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是基于 OpenCode 的大模型助手。请问有什么我可以帮助你的吗?你可以:\n1. 直接输入问题进行对话\n2. 上传文件进行分析\n3. 切换不同的模型\n\n例如,你可以问我:"如何使用 Python 实现快速排序?" 或者上传一个 PDF 文件让我分析其中的内容。',
|
||||
generatedFiles: []
|
||||
}
|
||||
]);
|
||||
setGeneratedFiles([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除会话失败:', err);
|
||||
setError('删除会话失败');
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!input.trim() && files.length === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setThinking(true);
|
||||
|
||||
try {
|
||||
const [modelType, modelName] = model.split('/');
|
||||
const modelId = model;
|
||||
|
||||
const newMessage = {
|
||||
role: 'user',
|
||||
content: input,
|
||||
files: files.map(file => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
}))
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
setInput('');
|
||||
|
||||
// 先将用户消息添加到缓存
|
||||
if (currentSession) {
|
||||
setSessionMessagesCache(cache => ({
|
||||
...cache,
|
||||
[currentSession.id]: [...(cache[currentSession.id] || []), newMessage]
|
||||
}));
|
||||
}
|
||||
let assistantContent = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
const formData = new FormData();
|
||||
files.forEach(file => formData.append('files', file));
|
||||
formData.append('messages', JSON.stringify([{ role: 'user', content: input }]));
|
||||
formData.append('model_type', modelType);
|
||||
formData.append('model_name', modelName);
|
||||
|
||||
const response = await axios.post('/api/chat/with-files', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
assistantContent = response.data.content;
|
||||
} else {
|
||||
console.log('发送消息时的会话ID:', currentSession?.id);
|
||||
const response = await axios.post('/api/v1/chat/completions', {
|
||||
messages: [{ role: 'user', content: input }],
|
||||
model: modelId,
|
||||
stream: false,
|
||||
session_id: currentSession?.id
|
||||
});
|
||||
|
||||
assistantContent = response.data.choices[0].message.content;
|
||||
}
|
||||
|
||||
const assistantMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
generatedFiles: extractGeneratedFiles(assistantContent)
|
||||
};
|
||||
|
||||
if (currentSession) {
|
||||
setSessionMessagesCache(cache => ({
|
||||
...cache,
|
||||
[currentSession.id]: [...(cache[currentSession.id] || []), assistantMessage]
|
||||
}));
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
|
||||
if (assistantMessage.generatedFiles.length > 0) {
|
||||
setGeneratedFiles(prev => [...prev, ...assistantMessage.generatedFiles]);
|
||||
}
|
||||
|
||||
setFiles([]);
|
||||
} catch (err) {
|
||||
console.error('发送消息失败:', err);
|
||||
setError('发送消息失败,请重试');
|
||||
setMessages(prev => prev.slice(0, -1));
|
||||
setInput(input);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setThinking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const extractGeneratedFiles = (content) => {
|
||||
const files = [];
|
||||
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||
const fileNameRegex = /^(?:file:?\s*)?[`"]?([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md))[`"]?/gim;
|
||||
const fileExistsRegex = /The files ([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md)) and ([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md)) already exist and are complete/g;
|
||||
const fileCreatedRegex = /已创建\s+([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md))(?:\s+文件)?/g;
|
||||
const filePathRegex = /`([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md))`/g;
|
||||
const listFileRegex = /-\s*\*{0,2}([\w./-]+\.(?:html|css|js|jsx|ts|tsx|py|sh|json|md))\*{0,2}\s*-/g;
|
||||
|
||||
// 提取代码块中的文件
|
||||
let match;
|
||||
while ((match = codeBlockRegex.exec(content)) !== null) {
|
||||
const language = match[1] || 'text';
|
||||
const code = match[2].trim();
|
||||
|
||||
const fileNameMatch = content.substring(0, match.index).match(fileNameRegex);
|
||||
const fileName = fileNameMatch
|
||||
? fileNameMatch[fileNameMatch.length - 1].replace(/[`"]/g, '')
|
||||
: `generated_code.${language === 'html' ? 'html' : language === 'javascript' ? 'js' : language}`;
|
||||
|
||||
files.push({
|
||||
name: fileName,
|
||||
language: language,
|
||||
content: code
|
||||
});
|
||||
}
|
||||
|
||||
// 提取文件已存在的信息
|
||||
match = fileExistsRegex.exec(content);
|
||||
if (match) {
|
||||
const file1 = match[1];
|
||||
const file2 = match[2];
|
||||
|
||||
// 为每个已存在的文件创建文件对象
|
||||
[file1, file2].forEach(fileName => {
|
||||
const language = fileName.split('.').pop();
|
||||
files.push({
|
||||
name: fileName,
|
||||
language: language,
|
||||
content: `// 已存在的文件: ${fileName}`,
|
||||
exists: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 提取文件已创建的信息
|
||||
while ((match = fileCreatedRegex.exec(content)) !== null) {
|
||||
const fileName = match[1];
|
||||
const language = fileName.split('.').pop();
|
||||
files.push({
|
||||
name: fileName,
|
||||
language: language,
|
||||
content: `// 已创建的文件: ${fileName}`,
|
||||
exists: true
|
||||
});
|
||||
}
|
||||
|
||||
// 提取文件路径信息(如 `index.html`)
|
||||
while ((match = filePathRegex.exec(content)) !== null) {
|
||||
let fileName = match[1];
|
||||
// 提取文件名(去除路径部分)
|
||||
fileName = fileName.split('/').pop();
|
||||
fileName = fileName.split('\\').pop();
|
||||
const language = fileName.split('.').pop();
|
||||
|
||||
// 检查文件是否已经在列表中
|
||||
const fileExists = files.some(file => file.name === fileName);
|
||||
if (!fileExists) {
|
||||
files.push({
|
||||
name: fileName,
|
||||
language: language,
|
||||
content: `// 文件: ${fileName}`,
|
||||
exists: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提取列表格式的文件路径(如 "- **square.html** - 包含一个带动画效果的正方形")
|
||||
while ((match = listFileRegex.exec(content)) !== null) {
|
||||
const fileName = match[1];
|
||||
const language = fileName.split('.').pop();
|
||||
|
||||
// 检查文件是否已经在列表中
|
||||
const fileExists = files.some(file => file.name === fileName);
|
||||
if (!fileExists) {
|
||||
files.push({
|
||||
name: fileName,
|
||||
language: language,
|
||||
content: `// 文件: ${fileName}`,
|
||||
exists: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const previewFile = async (file) => {
|
||||
try {
|
||||
const maxFileSize = 10 * 1024 * 1024;
|
||||
if (file.size > maxFileSize) {
|
||||
setError('文件大小超过限制(最大10MB)');
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['text/plain', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
setError('不支持的文件类型,请上传文本、PDF或Word文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewLoading(true);
|
||||
setError('');
|
||||
setPreviewContent('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
formData.append('messages', JSON.stringify([{ role: 'user', content: '请预览此文件内容,提取关键信息并以清晰的格式呈现' }]));
|
||||
formData.append('model_type', model.split('/')[0]);
|
||||
formData.append('model_name', model.split('/')[1]);
|
||||
|
||||
const response = await axios.post('/api/chat/with-files', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
setPreviewContent(response.data.content);
|
||||
} catch (err) {
|
||||
console.error('预览文件失败:', err);
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
setError('预览文件超时,请重试');
|
||||
} else {
|
||||
setError('预览文件失败,请重试');
|
||||
}
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const selectedFiles = Array.from(e.target.files);
|
||||
setFiles(prev => [...prev, ...selectedFiles]);
|
||||
};
|
||||
|
||||
const removeFile = (index) => {
|
||||
setFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleModelChange = async (e) => {
|
||||
const newModel = e.target.value;
|
||||
const [modelType, modelName] = newModel.split('/');
|
||||
|
||||
try {
|
||||
await axios.post('/api/model/switch', {
|
||||
model_type: modelType,
|
||||
model_name: modelName
|
||||
});
|
||||
setModel(newModel);
|
||||
} catch (err) {
|
||||
console.error('切换模型失败:', err);
|
||||
setError('切换模型失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const openPreview = async (file) => {
|
||||
if (file.exists) {
|
||||
// 如果文件已存在,尝试从后端获取文件内容
|
||||
try {
|
||||
setPreviewLoading(true);
|
||||
// 由于代理配置会重写路径,需要使用 /api/api/file/ 来正确映射到后端的 /api/file/ 路由
|
||||
// 处理文件路径,确保正确处理包含目录的文件
|
||||
let filePath = currentSession ? `${currentSession.id}/${file.name}` : file.name;
|
||||
const response = await axios.get(`/api/api/file/${encodeURIComponent(filePath)}`);
|
||||
// 创建一个新的文件对象,而不是修改现有对象
|
||||
let updatedFile = {
|
||||
...file,
|
||||
content: response.data.content,
|
||||
size: response.data.size,
|
||||
name: response.data.name
|
||||
};
|
||||
|
||||
// 如果是HTML文件,尝试内联CSS文件
|
||||
if (file.language === 'html' || file.name.endsWith('.html')) {
|
||||
updatedFile.content = await inlineCssFiles(updatedFile.content, filePath);
|
||||
}
|
||||
|
||||
setActivePreview(updatedFile);
|
||||
} catch (err) {
|
||||
console.error('获取文件内容失败:', err);
|
||||
setError('获取文件内容失败');
|
||||
setActivePreview(file); // 即使失败也显示文件信息
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
} else {
|
||||
// 直接显示生成的文件内容
|
||||
setActivePreview(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 内联CSS文件到HTML中
|
||||
const inlineCssFiles = async (htmlContent, htmlFilePath) => {
|
||||
try {
|
||||
// 提取HTML文件中的CSS引用
|
||||
const cssRegex = /<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
let updatedHtml = htmlContent;
|
||||
|
||||
while ((match = cssRegex.exec(htmlContent)) !== null) {
|
||||
const cssHref = match[1];
|
||||
// 只处理相对路径的CSS文件
|
||||
if (!cssHref.startsWith('http://') && !cssHref.startsWith('https://') && !cssHref.startsWith('/')) {
|
||||
// 构建CSS文件的完整路径
|
||||
const htmlDir = htmlFilePath.substring(0, htmlFilePath.lastIndexOf('/') + 1);
|
||||
const cssFilePath = htmlDir + cssHref;
|
||||
|
||||
// 获取CSS文件内容
|
||||
const cssResponse = await axios.get(`/api/api/file/${encodeURIComponent(cssFilePath)}`);
|
||||
const cssContent = cssResponse.data.content;
|
||||
|
||||
// 替换link标签为style标签
|
||||
const linkTag = match[0];
|
||||
const styleTag = `<style>${cssContent}</style>`;
|
||||
updatedHtml = updatedHtml.replace(linkTag, styleTag);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedHtml;
|
||||
} catch (err) {
|
||||
console.error('内联CSS文件失败:', err);
|
||||
return htmlContent; // 失败时返回原始内容
|
||||
}
|
||||
};
|
||||
|
||||
const closePreview = () => {
|
||||
setActivePreview(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 加载会话列表
|
||||
loadSessions();
|
||||
|
||||
if (messages.length === 0) {
|
||||
setMessages([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是基于 OpenCode 的大模型助手。请问有什么我可以帮助你的吗?你可以:\n1. 直接输入问题进行对话\n2. 上传文件进行分析\n3. 切换不同的模型\n\n例如,你可以问我:"如何使用 Python 实现快速排序?" 或者上传一个 PDF 文件让我分析其中的内容。',
|
||||
generatedFiles: []
|
||||
}
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, thinking]);
|
||||
|
||||
// 移除这个useEffect,避免覆盖会话列表的更新
|
||||
// useEffect(() => {
|
||||
// // 当会话列表变化时,更新会话列表
|
||||
// loadSessions();
|
||||
// }, [sessions.length]);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="layout">
|
||||
{/* 会话管理面板 */}
|
||||
<div className="layout__sessions">
|
||||
<div className="sessions-header">
|
||||
<h2>会话</h2>
|
||||
<button
|
||||
className="create-session-btn"
|
||||
onClick={() => setShowSessionModal(true)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
+ 新建会话
|
||||
</button>
|
||||
</div>
|
||||
<div className="sessions-list">
|
||||
{console.log('渲染会话列表:', sessions)}
|
||||
{sessions.length === 0 ? (
|
||||
<div className="no-sessions">
|
||||
没有会话,请创建一个新会话
|
||||
</div>
|
||||
) : (
|
||||
sessions.map(session => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`session-item ${currentSession && currentSession.id === session.id ? 'active' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="session-info"
|
||||
onClick={() => switchSession(session)}
|
||||
>
|
||||
<div className="session-title">{session.title}</div>
|
||||
<div className="session-time">
|
||||
{new Date(session.updated_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="session-delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('确定要删除这个会话吗?')) {
|
||||
deleteSession(session.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="layout__preview">
|
||||
<div className="preview-header">
|
||||
<h2>预览区</h2>
|
||||
<select
|
||||
className="model-selector"
|
||||
value={model}
|
||||
onChange={handleModelChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{modelOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="preview-content">
|
||||
{activePreview ? (
|
||||
<div className="code-preview">
|
||||
<div className="code-preview__header">
|
||||
<div>
|
||||
<span className="code-preview__filename">{activePreview.name}</span>
|
||||
{activePreview.size && (
|
||||
<span className="code-preview__filesize">({(activePreview.size / 1024).toFixed(2)} KB)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="code-preview__controls">
|
||||
<button
|
||||
className={`preview-mode-btn ${previewMode === 'code' ? 'active' : ''}`}
|
||||
onClick={() => setPreviewMode('code')}
|
||||
>
|
||||
源码
|
||||
</button>
|
||||
{activePreview.language === 'html' && (
|
||||
<button
|
||||
className={`preview-mode-btn ${previewMode === 'preview' ? 'active' : ''}`}
|
||||
onClick={() => setPreviewMode('preview')}
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
)}
|
||||
<button className="code-preview__close" onClick={closePreview}>×</button>
|
||||
</div>
|
||||
</div>
|
||||
{previewMode === 'code' ? (
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus}
|
||||
language={activePreview.language || 'html'}
|
||||
showLineNumbers
|
||||
>
|
||||
{activePreview.content}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<div className="html-preview">
|
||||
<iframe
|
||||
srcDoc={activePreview.content}
|
||||
title="HTML Preview"
|
||||
sandbox="allow-scripts"
|
||||
className="html-preview__iframe"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : previewContent ? (
|
||||
<div className="preview-markdown">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{previewContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="preview-placeholder">
|
||||
<p>选择左侧聊天中生成的文件进行预览</p>
|
||||
{generatedFiles.length > 0 && (
|
||||
<div className="generated-files-list">
|
||||
<h3>生成的文件:</h3>
|
||||
{generatedFiles.map((file, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="file-preview-btn"
|
||||
onClick={() => openPreview(file)}
|
||||
>
|
||||
{file.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="layout__chat">
|
||||
<div className="chat-container" ref={chatRef}>
|
||||
{messages.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`message ${msg.role === 'user' ? 'user-message' : 'assistant-message'}`}
|
||||
>
|
||||
<div className="message-content">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{msg.files && msg.files.length > 0 && (
|
||||
<div className="message-files">
|
||||
{msg.files.map((file, fileIndex) => (
|
||||
<div key={fileIndex} className="file-item">
|
||||
<span className="file-name">{file.name}</span>
|
||||
<span className="file-size">({(file.size / 1024).toFixed(2)} KB)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.generatedFiles && msg.generatedFiles.length > 0 && (
|
||||
<div className="message-generated-files">
|
||||
<span className="generated-files-label">生成的文件:</span>
|
||||
{msg.generatedFiles.map((file, fileIndex) => (
|
||||
<button
|
||||
key={fileIndex}
|
||||
className="file-preview-btn"
|
||||
onClick={() => openPreview(file)}
|
||||
>
|
||||
{file.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{thinking && (
|
||||
<div className="message assistant-message">
|
||||
<div className="thinking">正在思考...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="upload-section">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileUpload}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{files.length > 0 && (
|
||||
<div className="file-list">
|
||||
<h4>已选择的文件:</h4>
|
||||
{files.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<span>{file.name}</span>
|
||||
<button
|
||||
onClick={() => previewFile(file)}
|
||||
disabled={isLoading || previewLoading}
|
||||
>
|
||||
{previewLoading ? '预览中...' : '预览'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="input-section">
|
||||
<textarea
|
||||
placeholder="输入消息..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
disabled={isLoading}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={isLoading || (!input.trim() && files.length === 0)}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建会话模态框 */}
|
||||
{showSessionModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>创建新会话</h3>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={() => setShowSessionModal(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="会话标题"
|
||||
value={sessionInput}
|
||||
onChange={(e) => setSessionInput(e.target.value)}
|
||||
className="session-title-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className="modal-cancel"
|
||||
onClick={() => setShowSessionModal(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="modal-confirm"
|
||||
onClick={() => createSession(sessionInput)}
|
||||
disabled={!sessionInput.trim()}
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
.contact {
|
||||
padding: 6rem 0;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.contact-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: #1a1a1a;
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.contact-info-title {
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.contact-info-text {
|
||||
color: #888;
|
||||
margin: 0 0 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ff6a00;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-text strong {
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.info-text span {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
background: #ff6a00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 1.2rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #ff6a00;
|
||||
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #ff6a00, #ff8533);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 106, 0, 0.4);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover .arrow {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.contact-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { useState } from 'react'
|
||||
import './Contact.css'
|
||||
|
||||
const Contact = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }))
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
alert('Message sent! I will get back to you soon.')
|
||||
setFormData({ name: '', email: '', subject: '', message: '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="contact">
|
||||
<div className="section-container">
|
||||
<div className="section-header">
|
||||
<span className="section-tag">Get In Touch</span>
|
||||
<h2 className="section-title">Let's Work <span className="highlight">Together</span></h2>
|
||||
<p className="section-subtitle">
|
||||
Have a project in mind? I'd love to hear about it. Send me a message and let's create something amazing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="contact-content">
|
||||
<div className="contact-info">
|
||||
<h3 className="contact-info-title">Contact Information</h3>
|
||||
<p className="contact-info-text">
|
||||
Fill out the form and I'll get back to you within 24 hours.
|
||||
</p>
|
||||
|
||||
<div className="info-items">
|
||||
<div className="info-item">
|
||||
<span className="info-icon">✉</span>
|
||||
<div className="info-text">
|
||||
<strong>Email</strong>
|
||||
<span>sarah@designpro.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-icon">✆</span>
|
||||
<div className="info-text">
|
||||
<strong>Phone</strong>
|
||||
<span>+1 (555) 123-4567</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-icon">⚑</span>
|
||||
<div className="info-text">
|
||||
<strong>Location</strong>
|
||||
<span>San Francisco, CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="social-links">
|
||||
<a href="#" className="social-link" aria-label="LinkedIn">in</a>
|
||||
<a href="#" className="social-link" aria-label="Twitter">X</a>
|
||||
<a href="#" className="social-link" aria-label="Dribbble">Dr</a>
|
||||
<a href="#" className="social-link" aria-label="Behance">Be</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="contact-form" onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="john@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="subject">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
placeholder="Project Inquiry"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="message">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Tell me about your project..."
|
||||
rows={5}
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit-btn">
|
||||
Send Message
|
||||
<span className="arrow">➤</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Contact
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
.experience {
|
||||
padding: 6rem 0;
|
||||
background: #0a0a0a;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, #ff6a00, rgba(255, 106, 0, 0.1));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
margin-bottom: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item.left {
|
||||
justify-content: flex-start;
|
||||
padding-right: calc(50% + 40px);
|
||||
}
|
||||
|
||||
.timeline-item.right {
|
||||
justify-content: flex-end;
|
||||
padding-left: calc(50% + 40px);
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #ff6a00;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 0 20px rgba(255, 106, 0, 0.4);
|
||||
}
|
||||
|
||||
.timeline-dot::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 106, 0, 0.3);
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: #1a1a1a;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.timeline-content:hover {
|
||||
border-color: rgba(255, 106, 0, 0.2);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.timeline-period {
|
||||
display: inline-block;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
color: #ff6a00;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 1.3rem;
|
||||
color: #fff;
|
||||
margin: 0 0 0.3rem;
|
||||
}
|
||||
|
||||
.timeline-company {
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
color: #aaa;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.timeline-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.timeline-tag {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ccc;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.timeline-line {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.timeline-item.left,
|
||||
.timeline-item.right {
|
||||
padding-left: 50px;
|
||||
padding-right: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import './Experience.css'
|
||||
|
||||
const experiences = [
|
||||
{
|
||||
period: '2022 - Present',
|
||||
title: 'Senior UI/UX Designer',
|
||||
company: 'TechFlow Studio',
|
||||
description: 'Leading design team for enterprise SaaS products, creating design systems and improving user engagement by 40%.',
|
||||
tags: ['Design Systems', 'Leadership', 'SaaS']
|
||||
},
|
||||
{
|
||||
period: '2019 - 2022',
|
||||
title: 'UI/UX Designer',
|
||||
company: 'Creative Digital Agency',
|
||||
description: 'Designed 100+ websites and web applications for clients across healthcare, fintech, and e-commerce sectors.',
|
||||
tags: ['Web Design', 'Mobile Apps', 'Branding']
|
||||
},
|
||||
{
|
||||
period: '2017 - 2019',
|
||||
title: 'Visual Designer',
|
||||
company: 'Pixel Perfect Co.',
|
||||
description: 'Created brand identities, marketing materials, and digital assets for startups and established brands.',
|
||||
tags: ['Branding', 'Print Design', 'Visual Identity']
|
||||
},
|
||||
{
|
||||
period: '2014 - 2017',
|
||||
title: 'Junior Web Designer',
|
||||
company: 'StartUp Hub',
|
||||
description: 'Built responsive websites and landing pages for early-stage startups, learning rapid prototyping and agile design.',
|
||||
tags: ['Responsive Design', 'Prototyping', 'Startups']
|
||||
}
|
||||
]
|
||||
|
||||
const Experience = () => {
|
||||
return (
|
||||
<section id="experience" className="experience">
|
||||
<div className="section-container">
|
||||
<div className="section-header">
|
||||
<span className="section-tag">My Journey</span>
|
||||
<h2 className="section-title">Work <span className="highlight">Experience</span></h2>
|
||||
<p className="section-subtitle">
|
||||
A decade of crafting digital experiences for companies worldwide
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="timeline">
|
||||
{experiences.map((exp, index) => (
|
||||
<div key={index} className={`timeline-item ${index % 2 === 0 ? 'left' : 'right'}`}>
|
||||
<div className="timeline-dot"></div>
|
||||
<div className="timeline-content">
|
||||
<span className="timeline-period">{exp.period}</span>
|
||||
<h3 className="timeline-title">{exp.title}</h3>
|
||||
<span className="timeline-company">{exp.company}</span>
|
||||
<p className="timeline-description">{exp.description}</p>
|
||||
<div className="timeline-tags">
|
||||
{exp.tags.map((tag, i) => (
|
||||
<span key={i} className="timeline-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="timeline-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Experience
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
.timeline {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timelineItem {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timelineItem:last-child .dotWrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dotWrapper {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #ff6b22;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 20px rgba(255, 107, 34, 0.3);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 60px;
|
||||
background: rgba(255, 107, 34, 0.15);
|
||||
}
|
||||
|
||||
.timelineItem:last-child .line { display: none; }
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem 2rem;
|
||||
flex: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: rgba(255, 107, 34, 0.3);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.year {
|
||||
display: inline-block;
|
||||
background: rgba(255, 107, 34, 0.1);
|
||||
color: #ff8544;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.company {
|
||||
color: #a0a0a0;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #888888;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.timelineItem { gap: 1rem; }
|
||||
.card { padding: 1.25rem; }
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import './Footer.css'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="section-container">
|
||||
<div className="footer-content">
|
||||
<div className="footer-main">
|
||||
<div className="footer-logo">
|
||||
<span className="logo-accent">Design</span>Pro
|
||||
</div>
|
||||
<p className="footer-desc">
|
||||
Crafting intuitive digital experiences that connect brands with their audiences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="footer-links">
|
||||
<h4>Quick Links</h4>
|
||||
<ul>
|
||||
<li><button onClick={() => document.getElementById('hero')?.scrollIntoView({ behavior: 'smooth' })}>Home</button></li>
|
||||
<li><button onClick={() => document.getElementById('services')?.scrollIntoView({ behavior: 'smooth' })}>Services</button></li>
|
||||
<li><button onClick={() => document.getElementById('portfolio')?.scrollIntoView({ behavior: 'smooth' })}>Portfolio</button></li>
|
||||
<li><button onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}>Contact</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="footer-links">
|
||||
<h4>Services</h4>
|
||||
<ul>
|
||||
<li>UI/UX Design</li>
|
||||
<li>Web Design</li>
|
||||
<li>Landing Pages</li>
|
||||
<li>Brand Identity</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="footer-cta">
|
||||
<h4>Ready to start?</h4>
|
||||
<a href="mailto:sarah@designpro.com" className="cta-btn">Get in Touch</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-bottom">
|
||||
<p>© 2024 DesignPro. All rights reserved.</p>
|
||||
<div className="footer-socials">
|
||||
<a href="#">LinkedIn</a>
|
||||
<a href="#">Twitter</a>
|
||||
<a href="#">Dribbble</a>
|
||||
<a href="#">Behance</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
.hero-bg-shapes {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: #ff6a00;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: #ff8533;
|
||||
bottom: -50px;
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #ff4500;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
animation: fadeInUp 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
color: #fff;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #ff6a00;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: #ff8533;
|
||||
margin: 0 0 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.1rem;
|
||||
color: #aaa;
|
||||
line-height: 1.8;
|
||||
margin: 0 0 2rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #ff6a00;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-top: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.9rem 2rem;
|
||||
border-radius: 50px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #ff6a00, #ff8533);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 15px rgba(255, 106, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 106, 0, 0.6);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: #ff6a00;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
animation: fadeInRight 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
.avatar-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #ff6a00;
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.avatar-ring::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed rgba(255, 106, 0, 0.3);
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
position: absolute;
|
||||
inset: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #2a2a2a, #333);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 0 40px rgba(255, 106, 0, 0.2);
|
||||
}
|
||||
|
||||
.avatar-placeholder span {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.floating-badge {
|
||||
position: absolute;
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
border: 1px solid rgba(255, 106, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.badge-1 {
|
||||
top: 20px;
|
||||
right: -20px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.badge-2 {
|
||||
bottom: 40px;
|
||||
left: -30px;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
color: #ff6a00;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from { opacity: 0; transform: translateX(30px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import './Hero.css'
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section id="hero" className="hero">
|
||||
<div className="hero-bg-shapes">
|
||||
<div className="shape shape-1"></div>
|
||||
<div className="shape shape-2"></div>
|
||||
<div className="shape shape-3"></div>
|
||||
</div>
|
||||
|
||||
<div className="hero-container">
|
||||
<div className="hero-content">
|
||||
<h1 className="hero-title">
|
||||
Hello, I'm <span className="highlight">Sarah Chen</span>
|
||||
</h1>
|
||||
<p className="hero-subtitle">Senior Product Designer</p>
|
||||
<p className="hero-description">
|
||||
Crafting intuitive digital experiences that connect brands with their audiences.
|
||||
Specializing in UI/UX design, web design, and high-converting landing pages.
|
||||
</p>
|
||||
|
||||
<div className="hero-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-number">10</span>
|
||||
<span className="stat-label">Years Experience</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-number">450+</span>
|
||||
<span className="stat-label">Clients Served</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-number">★5.0</span>
|
||||
<span className="stat-label">Client Rating</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hero-buttons">
|
||||
<button className="btn btn-primary" onClick={() => document.getElementById('portfolio')?.scrollIntoView({ behavior: 'smooth' })}>
|
||||
View My Work
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}>
|
||||
Let's Collaborate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hero-image">
|
||||
<div className="avatar-wrapper">
|
||||
<div className="avatar-ring"></div>
|
||||
<div className="avatar-placeholder">
|
||||
<span>SC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="floating-badge badge-1">
|
||||
<span className="badge-icon">★</span>
|
||||
<span>Top Rated</span>
|
||||
</div>
|
||||
<div className="floating-badge badge-2">
|
||||
<span className="badge-icon">✓</span>
|
||||
<span>Available</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
.hero {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bgGradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 60% 50%, rgba(255, 107, 34, 0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 120px 20px 80px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 3rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInUp 0.6s ease both;
|
||||
}
|
||||
|
||||
.avatarRing {
|
||||
position: absolute;
|
||||
inset: -6px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #ff6b22;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4px solid rgba(255, 107, 34, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatarLetter {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #0a0a0a;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: rgba(255, 107, 34, 0.1);
|
||||
color: #ff8544;
|
||||
padding: 8px 16px;
|
||||
border-radius: 50px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255, 107, 34, 0.2);
|
||||
animation: fadeInUp 0.6s ease 0.1s both;
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -1px;
|
||||
animation: fadeInUp 0.6s ease 0.2s both;
|
||||
}
|
||||
|
||||
.orangeText {
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 1.5rem;
|
||||
animation: fadeInUp 0.6s ease 0.3s both;
|
||||
}
|
||||
|
||||
.subTitle span {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
color: #888888;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInUp 0.6s ease 0.4s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.statsSection {
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
margin-bottom: 2.5rem;
|
||||
animation: fadeInUp 0.6s ease 0.5s both;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: #ff6b22;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 0.85rem;
|
||||
color: #a0a0a0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
animation: fadeInUp 0.6s ease 0.6s both;
|
||||
}
|
||||
|
||||
.btnPrimary {
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
color: #fff;
|
||||
padding: 16px 32px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btnPrimary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(255, 107, 34, 0.4);
|
||||
}
|
||||
|
||||
.btnSecondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
padding: 16px 32px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btnSecondary:hover {
|
||||
border-color: #ff6b22;
|
||||
color: #ff6b22;
|
||||
background: rgba(255, 107, 34, 0.05);
|
||||
}
|
||||
|
||||
.sideElements {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.floatingCard {
|
||||
position: absolute;
|
||||
background: rgba(20, 20, 20, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floatingCard:nth-child(2) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.floatingEmoji {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.floatingTitle {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.floatingText {
|
||||
font-size: 0.8rem;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
.sideElements { display: none; }
|
||||
.mainTitle { font-size: 2.5rem; }
|
||||
.statsSection { gap: 2rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mainTitle { font-size: 2rem; }
|
||||
.buttonGroup { flex-direction: column; }
|
||||
.statsSection { flex-wrap: wrap; justify-content: center; }
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 1rem 0;
|
||||
transition: all 0.3s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
|
||||
padding: 0.7rem 0;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo-accent {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 2rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-links button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0;
|
||||
position: relative;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-links button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #ff6a00;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-links button:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.nav-links button:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-toggle span {
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-toggle span.open:nth-child(1) {
|
||||
transform: rotate(45deg) translate(5px, 5px);
|
||||
}
|
||||
|
||||
.menu-toggle span.open:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menu-toggle span.open:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(5px, -5px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.menu-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
width: 70%;
|
||||
height: 100vh;
|
||||
background: rgba(20, 20, 20, 0.98);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
transition: right 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-links.active {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import './Navbar.css'
|
||||
|
||||
const Navbar = () => {
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 50)
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
const scrollToSection = (id) => {
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
|
||||
setMenuOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={`navbar ${scrolled ? 'scrolled' : ''}`}>
|
||||
<div className="navbar-container">
|
||||
<div className="navbar-logo" onClick={() => scrollToSection('hero')}>
|
||||
<span className="logo-accent">Design</span>Pro
|
||||
</div>
|
||||
|
||||
<button className="menu-toggle" onClick={() => setMenuOpen(!menuOpen)}>
|
||||
<span className={menuOpen ? 'open' : ''}></span>
|
||||
<span className={menuOpen ? 'open' : ''}></span>
|
||||
<span className={menuOpen ? 'open' : ''}></span>
|
||||
</button>
|
||||
|
||||
<ul className={`nav-links ${menuOpen ? 'active' : ''}`}>
|
||||
{['hero', 'services', 'experience', 'portfolio', 'testimonials', 'contact'].map((section) => (
|
||||
<li key={section}>
|
||||
<button onClick={() => scrollToSection(section)}>
|
||||
{section.charAt(0).toUpperCase() + section.slice(1)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: transparent;
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.scrolled {
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logoIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logoAccent {
|
||||
color: #ff6b22;
|
||||
}
|
||||
|
||||
.navLinks {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navLinks a {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #a0a0a0;
|
||||
transition: color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navLinks a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.navLinks a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #ff6b22;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.navLinks a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ctaBtn {
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
color: #fff;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
animation: slideIn 0.6s ease 0.3s both;
|
||||
}
|
||||
|
||||
.ctaBtn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 107, 34, 0.4);
|
||||
}
|
||||
|
||||
.mobileToggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: #ffffff;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.barOpen:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); }
|
||||
.barOpen:nth-child(2) { opacity: 0; }
|
||||
.barOpen:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ctaBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileToggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navLinks {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(10, 10, 10, 0.98);
|
||||
flex-direction: column;
|
||||
padding: 2rem;
|
||||
gap: 1.5rem;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navLinks.open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
.portfolio {
|
||||
padding: 6rem 0;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.portfolio-filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #888;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
border-color: #ff6a00;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #ff6a00;
|
||||
border-color: #ff6a00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.portfolio-carousel {
|
||||
position: relative;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.carousel-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 106, 0, 0.2);
|
||||
border: 1px solid rgba(255, 106, 0, 0.3);
|
||||
color: #ff6a00;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: #ff6a00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.carousel-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
gap: 2rem;
|
||||
background: #1a1a1a;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.project-preview {
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-mockup {
|
||||
background: #0d0d0d;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mockup-header {
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.red { background: #ff5f56; }
|
||||
.dot.yellow { background: #ffbd2e; }
|
||||
.dot.green { background: #27c93f; }
|
||||
|
||||
.mockup-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.mockup-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.mockup-tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mockup-tag {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #888;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.project-category {
|
||||
color: #ff6a00;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1.8rem;
|
||||
color: #fff;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
color: #888;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.project-tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.project-tag {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ccc;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.view-project {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
background: #ff6a00;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.project-mini-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-mini-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(255, 106, 0, 0.2);
|
||||
}
|
||||
|
||||
.mini-gradient {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.mini-title {
|
||||
color: #fff;
|
||||
padding: 1rem 1rem 0.3rem;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.mini-category {
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.carousel-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.carousel-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { useState } from 'react'
|
||||
import './Portfolio.css'
|
||||
|
||||
const projects = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'FinTech App Redesign',
|
||||
category: 'UI/UX Design',
|
||||
description: 'Complete redesign of a mobile banking application, increasing user engagement by 45%.',
|
||||
tags: ['Mobile', 'FinTech', 'UX Research'],
|
||||
color: '#ff6a00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'E-Commerce Landing Page',
|
||||
category: 'Landing Page',
|
||||
description: 'High-converting landing page for a fashion e-commerce brand, 3x conversion rate.',
|
||||
tags: ['E-Commerce', 'Conversion', 'Responsive'],
|
||||
color: '#ff8533'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'SaaS Dashboard',
|
||||
category: 'UI/UX Design',
|
||||
description: 'Analytics dashboard for enterprise SaaS with complex data visualization.',
|
||||
tags: ['Dashboard', 'Data Viz', 'Enterprise'],
|
||||
color: '#ff4500'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Healthcare Platform',
|
||||
category: 'Web Design',
|
||||
description: 'Telemedicine platform connecting patients with healthcare providers seamlessly.',
|
||||
tags: ['Healthcare', 'Platform', 'Accessibility'],
|
||||
color: '#ff6a00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Restaurant Booking Page',
|
||||
category: 'Landing Page',
|
||||
description: 'Elegant landing page for a premium restaurant chain with online reservation system.',
|
||||
tags: ['Hospitality', 'Booking', 'Premium'],
|
||||
color: '#ff8533'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'EdTech Website',
|
||||
category: 'Web Design',
|
||||
description: 'Modern educational platform website with course catalog and student portal.',
|
||||
tags: ['Education', 'LMS', 'User Portal'],
|
||||
color: '#ff4500'
|
||||
}
|
||||
]
|
||||
|
||||
const categories = ['All', 'UI/UX Design', 'Web Design', 'Landing Page']
|
||||
|
||||
const Portfolio = () => {
|
||||
const [activeCategory, setActiveCategory] = useState('All')
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
|
||||
const filteredProjects = activeCategory === 'All'
|
||||
? projects
|
||||
: projects.filter(p => p.category === activeCategory)
|
||||
|
||||
const featuredProject = filteredProjects[currentIndex % filteredProjects.length]
|
||||
|
||||
const goTo = (direction) => {
|
||||
setCurrentIndex(prev => {
|
||||
const next = prev + direction
|
||||
return next < 0 ? filteredProjects.length - 1 : next >= filteredProjects.length ? 0 : next
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="portfolio" className="portfolio">
|
||||
<div className="section-container">
|
||||
<div className="section-header">
|
||||
<span className="section-tag">My Work</span>
|
||||
<h2 className="section-title">Featured <span className="highlight">Projects</span></h2>
|
||||
<p className="section-subtitle">
|
||||
A selection of projects that showcase my expertise and approach to design
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="portfolio-filters">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
className={`filter-btn ${activeCategory === cat ? 'active' : ''}`}
|
||||
onClick={() => { setActiveCategory(cat); setCurrentIndex(0); }}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="portfolio-carousel">
|
||||
<div className="carousel-nav">
|
||||
<button className="nav-btn prev" onClick={() => goTo(-1)}>❮</button>
|
||||
<button className="nav-btn next" onClick={() => goTo(1)}>❯</button>
|
||||
</div>
|
||||
|
||||
<div className="carousel-content">
|
||||
<div className="project-preview" style={{ background: `linear-gradient(135deg, ${featuredProject.color}20, ${featuredProject.color}05)` }}>
|
||||
<div className="preview-mockup">
|
||||
<div className="mockup-header">
|
||||
<span className="dot red"></span>
|
||||
<span className="dot yellow"></span>
|
||||
<span className="dot green"></span>
|
||||
</div>
|
||||
<div className="mockup-body">
|
||||
<div className="mockup-title" style={{ color: featuredProject.color }}>{featuredProject.title}</div>
|
||||
<div className="mockup-tags">
|
||||
{featuredProject.tags.map((tag, i) => (
|
||||
<span key={i} className="mockup-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="project-info">
|
||||
<span className="project-category">{featuredProject.category}</span>
|
||||
<h3 className="project-title">{featuredProject.title}</h3>
|
||||
<p className="project-description">{featuredProject.description}</p>
|
||||
<div className="project-tags">
|
||||
{featuredProject.tags.map((tag, i) => (
|
||||
<span key={i} className="project-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn btn-primary view-project">View Case Study</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="carousel-indicators">
|
||||
{filteredProjects.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`indicator ${i === currentIndex ? 'active' : ''}`}
|
||||
onClick={() => setCurrentIndex(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="projects-grid">
|
||||
{filteredProjects.slice(0, 3).map(project => (
|
||||
<div key={project.id} className="project-mini-card">
|
||||
<div className="mini-gradient" style={{ background: `linear-gradient(135deg, ${project.color}, ${project.color}80)` }}></div>
|
||||
<h4 className="mini-title">{project.title}</h4>
|
||||
<span className="mini-category">{project.category}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Portfolio
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
.filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterBtn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #a0a0a0;
|
||||
padding: 10px 24px;
|
||||
border-radius: 30px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.filterBtn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filterBtn.active {
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-color);
|
||||
background: var(--bg-card);
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.arrow:hover:not(:disabled) {
|
||||
border-color: #ff6b22;
|
||||
color: #ff6b22;
|
||||
}
|
||||
|
||||
.arrow:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.carouselInner {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.projectCard {
|
||||
flex: 0 0 calc((100% - 4rem) / 3);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.projectCard:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.projectPreview {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.projectHighlight {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.projectInfo {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.projectCategory {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b22;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.projectTitle {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.projectDesc {
|
||||
font-size: 0.9rem;
|
||||
color: #888888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.activeDot {
|
||||
background: #ff6b22;
|
||||
width: 24px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.projectCard {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.carouselInner {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.arrow { display: none; }
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
.services {
|
||||
padding: 6rem 0;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.section-tag {
|
||||
display: inline-block;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
color: #ff6a00;
|
||||
padding: 0.4rem 1.2rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2.8rem;
|
||||
color: #fff;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
color: #888;
|
||||
font-size: 1.1rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: rgba(255, 106, 0, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.4rem;
|
||||
color: #fff;
|
||||
margin: 0 0 0.8rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #888;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.card-preview {
|
||||
background: #0d0d0d;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 0.6rem 1rem;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.preview-dot.red { background: #ff5f56; }
|
||||
.preview-dot.yellow { background: #ffbd2e; }
|
||||
.preview-dot.green { background: #27c93f; }
|
||||
|
||||
.preview-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-preview {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.screen-mockup {
|
||||
flex: 1;
|
||||
height: 100px;
|
||||
background: linear-gradient(180deg, #222, #1a1a1a);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #555;
|
||||
font-size: 0.7rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.website-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wp-header {
|
||||
height: 30px;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.8rem;
|
||||
color: #ff6a00;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.wp-hero {
|
||||
height: 50px;
|
||||
background: linear-gradient(90deg, rgba(255, 106, 0, 0.2), rgba(255, 106, 0, 0.05));
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.8rem;
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.wp-grid {
|
||||
height: 40px;
|
||||
background: #222;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.8rem;
|
||||
color: #555;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.landing-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.lp-hero {
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #ff6a00, #ff8533);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.8rem;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lp-features {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.lp-features span {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
background: #222;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #555;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.card-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.card-features li {
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.check {
|
||||
color: #ff6a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 106, 0, 0.3);
|
||||
color: #ff6a00;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-btn:hover {
|
||||
background: #ff6a00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import './Services.css'
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'UI/UX Design',
|
||||
description: 'User-centered design solutions that create seamless experiences and drive engagement across all digital touchpoints.',
|
||||
icon: '📱',
|
||||
preview: {
|
||||
type: 'app',
|
||||
screens: ['Dashboard', 'Profile', 'Settings']
|
||||
},
|
||||
features: ['User Research', 'Wireframing', 'Prototyping', 'Usability Testing']
|
||||
},
|
||||
{
|
||||
title: 'Web Design',
|
||||
description: 'Modern, responsive websites that combine aesthetic appeal with optimal functionality and performance.',
|
||||
icon: '🌐',
|
||||
preview: {
|
||||
type: 'website',
|
||||
elements: ['Header', 'Hero Section', 'Content Grid']
|
||||
},
|
||||
features: ['Responsive Design', 'CMS Integration', 'SEO Optimization', 'Performance']
|
||||
},
|
||||
{
|
||||
title: 'Landing Pages',
|
||||
description: 'High-converting landing pages designed to maximize leads, sales, and user engagement for your campaigns.',
|
||||
icon: '🚀',
|
||||
preview: {
|
||||
type: 'landing',
|
||||
elements: ['Hero CTA', 'Features', 'Testimonials']
|
||||
},
|
||||
features: ['A/B Testing', 'Conversion Optimization', 'Analytics', 'Fast Loading']
|
||||
}
|
||||
]
|
||||
|
||||
const Services = () => {
|
||||
return (
|
||||
<section id="services" className="services">
|
||||
<div className="section-container">
|
||||
<div className="section-header">
|
||||
<span className="section-tag">What I Do</span>
|
||||
<h2 className="section-title">My <span className="highlight">Services</span></h2>
|
||||
<p className="section-subtitle">
|
||||
Comprehensive design solutions tailored to elevate your brand and drive measurable results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="services-grid">
|
||||
{services.map((service, index) => (
|
||||
<div key={index} className="service-card">
|
||||
<div className="card-icon">{service.icon}</div>
|
||||
<h3 className="card-title">{service.title}</h3>
|
||||
<p className="card-description">{service.description}</p>
|
||||
|
||||
<div className="card-preview">
|
||||
<div className="preview-header">
|
||||
<span className="preview-dot red"></span>
|
||||
<span className="preview-dot yellow"></span>
|
||||
<span className="preview-dot green"></span>
|
||||
</div>
|
||||
<div className="preview-content">
|
||||
{service.preview.type === 'app' && (
|
||||
<div className="app-preview">
|
||||
{service.preview.screens.map((screen, i) => (
|
||||
<div key={i} className="screen-mockup">
|
||||
<span>{screen}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{service.preview.type === 'website' && (
|
||||
<div className="website-preview">
|
||||
<div className="wp-header">{service.preview.elements[0]}</div>
|
||||
<div className="wp-hero">{service.preview.elements[1]}</div>
|
||||
<div className="wp-grid">
|
||||
{service.preview.elements[2]}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.preview.type === 'landing' && (
|
||||
<div className="landing-preview">
|
||||
<div className="lp-hero">{service.preview.elements[0]}</div>
|
||||
<div className="lp-features">
|
||||
<span>{service.preview.elements[1]}</span>
|
||||
<span>{service.preview.elements[2]}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="card-features">
|
||||
{service.features.map((feature, i) => (
|
||||
<li key={i}>
|
||||
<span className="check">✓</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button className="card-btn">Learn More</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Services
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
.cardsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
border-color: rgba(255, 107, 34, 0.3);
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(255, 107, 34, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
color: #888888;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cardPreview {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.previewBlock1 {
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #1a0c00 0%, #2d1800 50%, #1a0c00 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previewBlock2 {
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #0c0c1a 0%, #1a1a2d 50%, #0c0c1a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previewBlock3 {
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #0c1a0c 0%, #1a2d1a 50%, #0c1a0c 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previewLabel {
|
||||
color: rgba(255, 107, 34, 0.7);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cardTags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(255, 107, 34, 0.08);
|
||||
color: #ff8544;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.cardsGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
.testimonials {
|
||||
padding: 6rem 0;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.testimonials-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.testimonial-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.testimonial-card:hover {
|
||||
border-color: rgba(255, 106, 0, 0.2);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.client-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff6a00, #ff8533);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.client-role {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rating {
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.star {
|
||||
color: #ff6a00;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.testimonial-text {
|
||||
color: #ccc;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 1.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.project-badge {
|
||||
display: inline-block;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.badge-label {
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
color: #ff6a00;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.testimonials-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import './Testimonials.css'
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: 'Emily Rodriguez',
|
||||
title: 'Marketing Director',
|
||||
company: 'TechVentures Inc.',
|
||||
avatar: 'ER',
|
||||
rating: 5,
|
||||
text: "Sarah transformed our product's user experience completely. Our engagement metrics improved by 60% after implementing her designs. Her attention to detail and user-centric approach made all the difference.",
|
||||
project: 'SaaS Platform Redesign'
|
||||
},
|
||||
{
|
||||
name: 'Marcus Chen',
|
||||
title: 'CEO & Founder',
|
||||
company: 'HealthSync App',
|
||||
avatar: 'MC',
|
||||
rating: 5,
|
||||
text: "Working with Sarah was a game-changer for our healthcare app. She understood our complex requirements and delivered an intuitive design that our users love. Highly recommend!",
|
||||
project: 'Mobile App Design'
|
||||
},
|
||||
{
|
||||
name: 'Sophia Anderson',
|
||||
title: 'E-commerce Manager',
|
||||
company: 'LuxeStyle Fashion',
|
||||
avatar: 'SA',
|
||||
rating: 5,
|
||||
text: "Our conversion rate tripled after Sarah redesigned our landing pages. She combines creativity with data-driven decisions to produce remarkable results. A true professional!",
|
||||
project: 'Landing Page Optimization'
|
||||
}
|
||||
]
|
||||
|
||||
const Testimonials = () => {
|
||||
return (
|
||||
<section id="testimonials" className="testimonials">
|
||||
<div className="section-container">
|
||||
<div className="section-header">
|
||||
<span className="section-tag">Testimonials</span>
|
||||
<h2 className="section-title">What Clients <span className="highlight">Say</span></h2>
|
||||
<p className="section-subtitle">
|
||||
Real feedback from clients who have experienced the impact of great design
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="testimonials-grid">
|
||||
{testimonials.map((item, index) => (
|
||||
<div key={index} className="testimonial-card">
|
||||
<div className="card-header">
|
||||
<div className="client-avatar">{item.avatar}</div>
|
||||
<div className="client-info">
|
||||
<h4 className="client-name">{item.name}</h4>
|
||||
<span className="client-role">{item.title}, {item.company}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rating">
|
||||
{Array.from({ length: item.rating }).map((_, i) => (
|
||||
<span key={i} className="star">★</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="testimonial-text">"{item.text}"</p>
|
||||
|
||||
<div className="project-badge">
|
||||
<span className="badge-label">Project:</span>
|
||||
<span className="badge-text">{item.project}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Testimonials
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
.why-choose-me {
|
||||
padding: 6rem 0;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.why-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.why-description {
|
||||
color: #aaa;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.why-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
|
||||
.why-list li {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.list-icon {
|
||||
color: #ff6a00;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-text strong {
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.list-text span {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.why-image {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.designer-illustration {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #1a1a1a, #222);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
min-height: 300px;
|
||||
border: 1px solid rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
|
||||
.illustration-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 30% 70%, rgba(255, 106, 0, 0.1), transparent);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.illustration-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.illustration-tools {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tool {
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: rgba(255, 106, 0, 0.15);
|
||||
border: 1px solid rgba(255, 106, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tool-2 { background: rgba(255, 133, 51, 0.15); }
|
||||
.tool-3 { background: rgba(255, 69, 0, 0.15); }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.stat-highlight {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
display: block;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.why-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import './WhyChooseMe.css'
|
||||
|
||||
const stats = [
|
||||
{ number: '600+', label: 'Projects Completed' },
|
||||
{ number: '50+', label: 'Industries Covered' },
|
||||
{ number: '450+', label: 'Happy Clients' },
|
||||
{ number: '15+', label: 'Design Awards' }
|
||||
]
|
||||
|
||||
const WhyChooseMe = () => {
|
||||
return (
|
||||
<section className="why-choose-me">
|
||||
<div className="section-container">
|
||||
<div className="why-content">
|
||||
<div className="why-text">
|
||||
<span className="section-tag">Why Me</span>
|
||||
<h2 className="section-title">Why Choose <span className="highlight">Me</span>?</h2>
|
||||
<p className="why-description">
|
||||
With over a decade of experience in digital design, I bring a unique combination
|
||||
of creative vision, technical expertise, and business understanding to every project.
|
||||
</p>
|
||||
|
||||
<ul className="why-list">
|
||||
<li>
|
||||
<span className="list-icon">✓</span>
|
||||
<div className="list-text">
|
||||
<strong>User-Centered Approach</strong>
|
||||
<span>Every design decision is backed by research and user feedback</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span className="list-icon">✓</span>
|
||||
<div className="list-text">
|
||||
<strong>Results-Driven Design</strong>
|
||||
<span>Focused on metrics that matter - conversions, engagement, retention</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span className="list-icon">✓</span>
|
||||
<div className="list-text">
|
||||
<strong>End-to-End Service</strong>
|
||||
<span>From concept to launch, handling every aspect of design</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button className="btn btn-primary" onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}>
|
||||
Start a Project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="why-image">
|
||||
<div className="designer-illustration">
|
||||
<div className="illustration-bg"></div>
|
||||
<div className="illustration-content">
|
||||
<div className="illustration-tools">
|
||||
<span className="tool tool-1">Figma</span>
|
||||
<span className="tool tool-2">Sketch</span>
|
||||
<span className="tool tool-3">Adobe</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="stat-card">
|
||||
<span className="stat-number stat-highlight">{stat.number}</span>
|
||||
<span className="stat-label">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhyChooseMe
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
.whySection {
|
||||
background: linear-gradient(180deg, #0a0a0a 0%, #0f0f0f 100%);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.leftContent .title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
text-align: left;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #888888;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.featureItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 1rem;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
color: #ff6b22;
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #ff6b22 0%, #ff8544 100%);
|
||||
color: #fff;
|
||||
padding: 16px 32px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(255, 107, 34, 0.4);
|
||||
}
|
||||
|
||||
.designerImage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.designerPlaceholder {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #222 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(255, 107, 34, 0.2);
|
||||
transform: rotate(-3deg);
|
||||
}
|
||||
|
||||
.designerIcon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.designerRole {
|
||||
color: #ff8544;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.glowRing {
|
||||
position: absolute;
|
||||
inset: -10px;
|
||||
border: 2px solid rgba(255, 107, 34, 0.08);
|
||||
border-radius: 24px;
|
||||
transform: rotate(-3deg);
|
||||
animation: pulse-ring 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0%, 100% { opacity: 0.5; transform: rotate(-3deg) scale(1); }
|
||||
50% { opacity: 1; transform: rotate(-3deg) scale(1.03); }
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.statCard:hover {
|
||||
border-color: rgba(255, 107, 34, 0.3);
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #ff6b22;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #888888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.leftContent .title {
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.btn { display: block; text-align: center; }
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.statsGrid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,672 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.App {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 会话管理面板 */
|
||||
.layout__sessions {
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #ddd;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sessions-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sessions-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.create-session-btn {
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.create-session-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.create-session-btn:disabled {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sessions-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 8px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.session-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.session-delete:hover {
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.layout__preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #ddd;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.layout__chat {
|
||||
width: 450px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.model-selector {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-selector option {
|
||||
color: #333;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.preview-placeholder p {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.generated-files-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.generated-files-list h3 {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.code-preview {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code-preview__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.code-preview__filename {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.code-preview__filesize {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.code-preview__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-mode-btn {
|
||||
padding: 4px 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #999;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.preview-mode-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.preview-mode-btn.active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.code-preview__close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.code-preview__close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.html-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
background-color: white;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.html-preview__iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.code-preview pre {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-markdown {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background-color: #e3f2fd;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: white;
|
||||
align-self: flex-start;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-content h1,
|
||||
.message-content h2,
|
||||
.message-content h3,
|
||||
.message-content h4,
|
||||
.message-content h5,
|
||||
.message-content h6 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-content h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.message-content h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.message-content h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.message-content ul,
|
||||
.message-content ol {
|
||||
margin-bottom: 1em;
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.message-content li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.message-content blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-content a {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.message-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.message-files {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.message-files .file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-files .file-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-files .file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-generated-files {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.generated-files-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.file-preview-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-preview-btn:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 10px 15px;
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.upload-section input[type="file"] {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.file-item button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-item button:first-of-type {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.input-section textarea {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
min-height: 60px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input-section button {
|
||||
padding: 0 20px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.input-section button:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.input-section button:disabled {
|
||||
background-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.thinking {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4f;
|
||||
margin: 10px 15px;
|
||||
padding: 10px;
|
||||
background-color: #fff1f0;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 创建会话模态框 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.session-title-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.session-title-input:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.modal-cancel {
|
||||
padding: 8px 16px;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-cancel:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.modal-confirm {
|
||||
padding: 8px 16px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.modal-confirm:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.modal-confirm:disabled {
|
||||
background-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,572 @@
|
|||
:root {
|
||||
--background: #fdf8f3;
|
||||
--background-secondary: #f5f0eb;
|
||||
--foreground: #262626;
|
||||
--accent: #e4a4bd;
|
||||
--accent-foreground: #262626;
|
||||
--card: #fdf8f3;
|
||||
--card-hover: #e4a4bd;
|
||||
--border: rgba(38, 38, 38, 0.05);
|
||||
--border-subtle: rgba(38, 38, 38, 0.08);
|
||||
--muted: rgba(38, 38, 38, 0.7);
|
||||
--muted-light: rgba(38, 38, 38, 0.3);
|
||||
--glass-bg: rgba(253, 248, 243, 0.8);
|
||||
--transition-luxury: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--font-family: 'League Spartan', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ==================== NAVIGATION ==================== */
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 48px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav__brand {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.nav__menu {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
transition: color 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.nav__link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav__cta {
|
||||
background: var(--accent);
|
||||
color: var(--accent-foreground);
|
||||
padding: 12px 32px;
|
||||
border-radius: 9999px;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
transition: all 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.nav__cta:hover {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
/* ==================== HERO SECTION ==================== */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 48px;
|
||||
align-items: center;
|
||||
padding: 120px 48px 48px;
|
||||
}
|
||||
|
||||
.hero__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.hero__headline {
|
||||
font-size: clamp(64px, 15vw, 180px);
|
||||
font-weight: 800;
|
||||
line-height: 0.8;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.hero__headline em {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hero__subtext {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
max-width: 400px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
padding-bottom: 8px;
|
||||
transition: all 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.hero__cta:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
|
||||
.hero__cta-arrow {
|
||||
transition: transform 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.hero__cta:hover .hero__cta-arrow {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.hero__image-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero__image {
|
||||
width: 100%;
|
||||
aspect-ratio: 3/4;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: grayscale(100%);
|
||||
transition: all 1s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.hero__image:hover img {
|
||||
filter: grayscale(0%);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.hero__badge {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
left: -40px;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: bounce-slow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hero__badge-number {
|
||||
font-size: 48px;
|
||||
font-style: italic;
|
||||
color: var(--accent-foreground);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hero__badge-text {
|
||||
font-size: 8px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-foreground);
|
||||
}
|
||||
|
||||
/* ==================== SERVICES SECTION ==================== */
|
||||
.services {
|
||||
background: var(--background-secondary);
|
||||
padding: 120px 48px;
|
||||
}
|
||||
|
||||
.services__headline {
|
||||
font-size: clamp(48px, 8vw, 96px);
|
||||
font-weight: 800;
|
||||
line-height: 0.8;
|
||||
letter-spacing: -0.04em;
|
||||
text-align: center;
|
||||
margin-bottom: 80px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.services__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.services__card {
|
||||
padding: 40px;
|
||||
background: var(--card);
|
||||
border-right: 1px solid var(--border-subtle);
|
||||
transition: all 1s var(--transition-luxury);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.services__card:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.services__card:hover {
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.services__icon {
|
||||
font-size: 48px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 24px;
|
||||
transition: color 1s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.services__card:hover .services__icon {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.services__card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 16px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.services__card-text {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ==================== PORTFOLIO SECTION ==================== */
|
||||
.portfolio {
|
||||
padding: 120px 48px;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.portfolio__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.portfolio__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.portfolio__item:nth-child(even) {
|
||||
transform: translateY(100px);
|
||||
}
|
||||
|
||||
.portfolio__image-wrapper {
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 3/4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.portfolio__image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: grayscale(100%);
|
||||
transition: all 1s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.portfolio__image-wrapper:hover img {
|
||||
filter: grayscale(0%);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.portfolio__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.portfolio__image-wrapper:hover .portfolio__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.portfolio__overlay-circle {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.1em;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.portfolio__category {
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.portfolio__title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.portfolio__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.portfolio__meta-separator {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ==================== FOOTER ==================== */
|
||||
.footer {
|
||||
background: var(--background-secondary);
|
||||
padding: 80px 48px 40px;
|
||||
}
|
||||
|
||||
.footer__main {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr 7fr;
|
||||
gap: 48px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.footer__brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer__logo {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.footer__mission {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.footer__columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.footer__column-title {
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.footer__column-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--accent);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.footer__column-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer__column-link {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.footer__column-link:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.footer__bottom {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding-top: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
font-size: 9px;
|
||||
color: var(--muted-light);
|
||||
}
|
||||
|
||||
.footer__legal {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer__legal-link {
|
||||
font-size: 9px;
|
||||
color: var(--muted-light);
|
||||
text-decoration: none;
|
||||
transition: color 0.6s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.footer__legal-link:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* ==================== ANIMATIONS ==================== */
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% {
|
||||
transform: translateY(-5%);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(5%);
|
||||
}
|
||||
}
|
||||
|
||||
.reveal-up {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: all 1s var(--transition-luxury);
|
||||
}
|
||||
|
||||
.reveal-up.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ==================== RESPONSIVE ==================== */
|
||||
@media (max-width: 1024px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 120px 24px 48px;
|
||||
}
|
||||
|
||||
.services__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.services__card {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.services__card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.portfolio__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portfolio__item:nth-child(even) {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.footer__main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.nav__menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero, .services, .portfolio, .footer {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.hero__badge {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
bottom: -20px;
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.hero__badge-number {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
# 前端 API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了前端可以调用的后端 API 接口,用于与 AI 服务进行交互。
|
||||
|
||||
## 基础 URL
|
||||
|
||||
- 本地开发:`http://localhost:8000`
|
||||
- 生产环境:js2.blockelite.cn
|
||||
- 端口:17032
|
||||
- SSH账户:root
|
||||
- 密码:zao3aiCh
|
||||
- SSH账户:vipuser
|
||||
- 密码:zao3aiCh
|
||||
- SSH登录命令:ssh root@js2.blockelite.cn -p 17032
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 会话管理
|
||||
|
||||
#### 1.1 获取所有会话
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/sessions`
|
||||
- 认证:无
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{
|
||||
"id": "string",
|
||||
"title": "string",
|
||||
"created_at": "number",
|
||||
"updated_at": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 创建新会话
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/sessions`
|
||||
- 认证:无
|
||||
- 请求体:
|
||||
```json
|
||||
{
|
||||
"title": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"title": "string",
|
||||
"created_at": "number",
|
||||
"updated_at": "number"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 获取会话详情
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/sessions/{session_id}`
|
||||
- 认证:无
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"title": "string",
|
||||
"created_at": "number",
|
||||
"updated_at": "number"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 获取会话消息
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/sessions/{session_id}/messages`
|
||||
- 认证:无
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "string",
|
||||
"session_id": "string",
|
||||
"role": "user" | "assistant",
|
||||
"content": "string",
|
||||
"created_at": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.5 发送消息
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/sessions/{session_id}/chat`
|
||||
- 认证:无
|
||||
- 请求体:
|
||||
```json
|
||||
{
|
||||
"message": "string",
|
||||
"model": "string",
|
||||
"stream": "boolean"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
- 非流式:
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"content": "string",
|
||||
"created_at": "number"
|
||||
}
|
||||
```
|
||||
- 流式:SSE (Server-Sent Events)
|
||||
|
||||
#### 1.6 删除会话
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`DELETE`
|
||||
- 路径:`/sessions/{session_id}`
|
||||
- 认证:无
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": "boolean"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. OpenAI 兼容接口
|
||||
|
||||
#### 2.1 聊天完成
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/v1/chat/completions`
|
||||
- 认证:无
|
||||
- 请求体:
|
||||
```json
|
||||
{
|
||||
"model": "string",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user" | "assistant" | "system",
|
||||
"content": "string"
|
||||
}
|
||||
],
|
||||
"stream": "boolean",
|
||||
"session_id": "string" (可选)
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
- 非流式:
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"object": "chat.completion",
|
||||
"created": "number",
|
||||
"model": "string",
|
||||
"choices": [
|
||||
{
|
||||
"index": "number",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "string"
|
||||
},
|
||||
"finish_reason": "string"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": "number",
|
||||
"completion_tokens": "number",
|
||||
"total_tokens": "number"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 流式:SSE (Server-Sent Events),格式为 OpenAI 兼容的 `data: [DONE]` 格式
|
||||
|
||||
### 3. 文件访问
|
||||
|
||||
#### 3.1 获取文件内容
|
||||
|
||||
**请求**:
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/api/file/{file_path}`
|
||||
- 认证:无
|
||||
|
||||
**响应**:
|
||||
|
||||
- 文件内容(根据文件类型返回相应的内容)
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 接口在遇到错误时,会返回以下格式的错误响应:
|
||||
|
||||
````json
|
||||
{
|
||||
"detail": "string"
|
||||
}
|
||||
|
||||
## 示例请求
|
||||
|
||||
### 示例 1:发送消息(非流式)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/sessions/{session_id}/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "你好",
|
||||
"model": "bailian-token-plan/qwen3.6-plus",
|
||||
"stream": false
|
||||
}'
|
||||
````
|
||||
|
||||
### 示例 2:使用 OpenAI 兼容接口
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "bailian-token-plan/qwen3.6-plus",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
```
|
||||
|
||||
## 模型列表
|
||||
|
||||
可用的模型列表:
|
||||
|
||||
- `bailian-token-plan/qwen3.6-plus` (百炼)
|
||||
- `bailian-token-plan/MiniMax-M2.5` (百炼)
|
||||
- `bailian-token-plan/glm-5` (百炼)
|
||||
- `bailian-token-plan/deepseek-v3.2` (百炼)
|
||||
- `ollama/qwen3-coder-next:latest` (Ollama)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **流式响应**:使用 `stream: true` 时,响应为 SSE 格式,前端需要使用 SSE 客户端库处理
|
||||
2. **会话管理**:所有消息都与会话关联,确保在发送消息时指定正确的 `session_id`
|
||||
3. **模型选择**:不同模型的能力和响应格式可能不同,请根据需要选择合适的模型
|
||||
4. **错误处理**:前端应处理 API 错误,特别是网络错误和服务不可用的情况
|
||||
5. **文件访问**:生成的文件会保存在会话目录中,通过 `/api/file/{session_id}/{filename}` 访问
|
||||
|
||||
## 部署配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
前端部署时需要配置以下环境变量:
|
||||
|
||||
- `VITE_API_URL`:后端 API 地址,例如 `http://localhost:8000`
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. 启动 opencode serve 服务:
|
||||
```bash
|
||||
OPENCODE_SERVER_PASSWORD=123456789 opencode serve --port 4096 --hostname 0.0.0.0
|
||||
```
|
||||
2. 启动后端服务:
|
||||
```bash
|
||||
uvicorn src.api.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
3. 启动前端服务:
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
### 容器部署
|
||||
|
||||
使用 docker-compose 部署:
|
||||
|
||||
```bash
|
||||
docker-compose -f deploy/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **v1.0.0**:初始版本,支持会话管理和 OpenAI 兼容接口
|
||||
- **v1.1.0**:添加 opencode serve 模式支持
|
||||
- **v1.2.0**:优化文件管理,支持会话级文件隔离
|
||||
|
||||
|
|
@ -14,8 +14,59 @@ const ProjectDetailContent: React.FC = () => {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const [iframeContent, setIframeContent] = useState<string>('');
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [annotations, setAnnotations] = useState<{ elementId: string; content: string }[]>([]);
|
||||
const { setSelectedElement } = useEditor();
|
||||
|
||||
const handleAIEdit = async () => {
|
||||
console.log('=== AI修改请求 ===');
|
||||
console.log('项目Id:', id);
|
||||
console.log('页面Id:', pageId);
|
||||
console.log('标注内容:', annotations);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/ai/edit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId: id,
|
||||
pageId: pageId,
|
||||
annotations: annotations,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('API request failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('=== AI修改响应 ===');
|
||||
console.log('返回的页面数据:', data);
|
||||
|
||||
if (data.content) {
|
||||
setIframeContent(data.content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('=== AI修改模拟响应 ===');
|
||||
console.log('项目Id:', id);
|
||||
console.log('页面Id:', pageId);
|
||||
console.log('标注内容:', annotations);
|
||||
|
||||
const mockResponse = `
|
||||
<div>
|
||||
<h1 class="editable-element" id="ai-title-${Date.now()}">AI 修改后的标题</h1>
|
||||
<p class="editable-element" id="ai-content-${Date.now()}">这是AI根据您的标注内容生成的新内容。项目: ${id}, 页面: ${pageId}</p>
|
||||
<p class="editable-element" id="ai-note-${Date.now()}">标注数量: ${annotations.length} 个</p>
|
||||
</div>
|
||||
`;
|
||||
setIframeContent(mockResponse);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const adjustZoom = (delta: number) => {
|
||||
setZoom(prevZoom => {
|
||||
const newZoom = Math.max(0.5, Math.min(2, prevZoom + delta));
|
||||
|
|
@ -350,6 +401,15 @@ const ProjectDetailContent: React.FC = () => {
|
|||
{/* 页面标题 */}
|
||||
<div className="p-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium text-gray-800">页面编辑 - 项目 {id} / 页面 {pageId}</h2>
|
||||
<button
|
||||
onClick={handleAIEdit}
|
||||
className="px-4 py-2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-lg hover:from-purple-600 hover:to-indigo-600 transition-all shadow-md hover:shadow-lg flex items-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI修改
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => adjustZoom(-0.1)}
|
||||
|
|
|
|||
|
|
@ -7,31 +7,31 @@ export const fetchProjectContent = async (projectId: string, pageId: string = '1
|
|||
const projectContents: Record<string, Record<string, string>> = {
|
||||
'1': {
|
||||
'1': `
|
||||
<div id="home-hero">
|
||||
<div class="editable-element" id="home-hero">
|
||||
<h1 class="editable-element" id="home-hero-title">Welcome to Our Website</h1>
|
||||
<p class="editable-element" id="home-hero-description">This is the hero section of the home page.</p>
|
||||
</div>
|
||||
<div id="home-features">
|
||||
<div class="editable-element" id="home-features">
|
||||
<h2 class="editable-element" id="home-features-title">Our Features</h2>
|
||||
<p class="editable-element" id="home-features-description">Check out our amazing features.</p>
|
||||
</div>
|
||||
`,
|
||||
'2': `
|
||||
<div id="about-header">
|
||||
<div class="editable-element" id="about-header">
|
||||
<h1 class="editable-element" id="about-header-title">About Us</h1>
|
||||
<p class="editable-element" id="about-header-description">Learn more about our company.</p>
|
||||
</div>
|
||||
<div id="about-history">
|
||||
<div class="editable-element" id="about-history">
|
||||
<h2 class="editable-element" id="about-history-title">Our History</h2>
|
||||
<p class="editable-element" id="about-history-description">A decade of innovation and growth.</p>
|
||||
</div>
|
||||
`,
|
||||
'3': `
|
||||
<div id="products-header">
|
||||
<div class="editable-element" id="products-header">
|
||||
<h1 class="editable-element" id="products-header-title">Our Products</h1>
|
||||
<p class="editable-element" id="products-header-description">Discover our amazing products.</p>
|
||||
</div>
|
||||
<div id="products-list">
|
||||
<div class="editable-element" id="products-list">
|
||||
<h2 class="editable-element" id="products-list-title">Product Categories</h2>
|
||||
<p class="editable-element" id="products-list-description">Browse our product categories.</p>
|
||||
</div>
|
||||
|
|
@ -39,31 +39,31 @@ export const fetchProjectContent = async (projectId: string, pageId: string = '1
|
|||
},
|
||||
'2': {
|
||||
'1': `
|
||||
<div id="home-hero">
|
||||
<div class="editable-element" id="home-hero">
|
||||
<h1 class="editable-element" id="home-hero-title">Welcome to Company B</h1>
|
||||
<p class="editable-element" id="home-hero-description">Your partner in business solutions.</p>
|
||||
</div>
|
||||
<div id="home-services">
|
||||
<div class="editable-element" id="home-services">
|
||||
<h2 class="editable-element" id="home-services-title">Our Services</h2>
|
||||
<p class="editable-element" id="home-services-description">Professional services for your business.</p>
|
||||
</div>
|
||||
`,
|
||||
'2': `
|
||||
<div id="services-header">
|
||||
<div class="editable-element" id="services-header">
|
||||
<h1 class="editable-element" id="services-header-title">Our Services</h1>
|
||||
<p class="editable-element" id="services-header-description">Comprehensive solutions for all your needs.</p>
|
||||
</div>
|
||||
<div id="services-details">
|
||||
<div class="editable-element" id="services-details">
|
||||
<h2 class="editable-element" id="services-details-title">Service Details</h2>
|
||||
<p class="editable-element" id="services-details-description">Learn more about each service we offer.</p>
|
||||
</div>
|
||||
`,
|
||||
'3': `
|
||||
<div id="contact-header">
|
||||
<div class="editable-element" id="contact-header">
|
||||
<h1 class="editable-element" id="contact-header-title">Contact Us</h1>
|
||||
<p class="editable-element" id="contact-header-description">Get in touch with our team.</p>
|
||||
</div>
|
||||
<div id="contact-form">
|
||||
<div class="editable-element" id="contact-form">
|
||||
<h2 class="editable-element" id="contact-form-title">Send a Message</h2>
|
||||
<p class="editable-element" id="contact-form-description">Fill out the form below to contact us.</p>
|
||||
</div>
|
||||
|
|
@ -71,31 +71,31 @@ export const fetchProjectContent = async (projectId: string, pageId: string = '1
|
|||
},
|
||||
'3': {
|
||||
'1': `
|
||||
<div id="home-hero">
|
||||
<div class="editable-element" id="home-hero">
|
||||
<h1 class="editable-element" id="home-hero-title">Welcome to Company C</h1>
|
||||
<p class="editable-element" id="home-hero-description">Innovation and excellence.</p>
|
||||
</div>
|
||||
<div id="home-news">
|
||||
<div class="editable-element" id="home-news">
|
||||
<h2 class="editable-element" id="home-news-title">Latest News</h2>
|
||||
<p class="editable-element" id="home-news-description">Stay updated with our latest developments.</p>
|
||||
</div>
|
||||
`,
|
||||
'2': `
|
||||
<div id="news-header">
|
||||
<div class="editable-element" id="news-header">
|
||||
<h1 class="editable-element" id="news-header-title">News & Updates</h1>
|
||||
<p class="editable-element" id="news-header-description">Latest news from our company.</p>
|
||||
</div>
|
||||
<div id="news-list">
|
||||
<div class="editable-element" id="news-list">
|
||||
<h2 class="editable-element" id="news-list-title">News Articles</h2>
|
||||
<p class="editable-element" id="news-list-description">Browse our latest news articles.</p>
|
||||
</div>
|
||||
`,
|
||||
'3': `
|
||||
<div id="support-header">
|
||||
<div class="editable-element" id="support-header">
|
||||
<h1 class="editable-element" id="support-header-title">Customer Support</h1>
|
||||
<p class="editable-element" id="support-header-description">We're here to help you.</p>
|
||||
</div>
|
||||
<div id="support-resources">
|
||||
<div class="editable-element" id="support-resources">
|
||||
<h2 class="editable-element" id="support-resources-title">Support Resources</h2>
|
||||
<p class="editable-element" id="support-resources-description">Find answers to common questions.</p>
|
||||
</div>
|
||||
|
|
@ -110,8 +110,8 @@ export const fetchProjectContent = async (projectId: string, pageId: string = '1
|
|||
|
||||
return `
|
||||
<div class="editable-element" id="default-element">
|
||||
<h1>Project ${projectId} - Page ${pageId}</h1>
|
||||
<p>This is the default content for project ${projectId}, page ${pageId}.</p>
|
||||
<h1 class="editable-element">Project ${projectId} - Page ${pageId}</h1>
|
||||
<p class="editable-element">This is the default content for project ${projectId}, page ${pageId}.</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
Loading…
Reference in New Issue