feat(ZEE-29): implement design system, dark mode, and JSON content management
- Add comprehensive design system with CSS variables, animations, and utility classes - Implement dark mode with ThemeProvider (system preference + manual toggle) - Create JSON-based content management system in src/content/ - Update all pages to use structured JSON content - Add ThemeProvider component with theme toggle button - Update Header with glass effect and animated mobile menu - Update Footer with dark mode support - Update documentation (README.md, CLAUDE.md, knowledge.md, proto/README.md) - Sync with Linear (ZEE-29 completed)
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Codebuff Agent Type Definitions
|
||||
*
|
||||
* This file provides TypeScript type definitions for creating custom Codebuff agents.
|
||||
* Import these types in your agent files to get full type safety and IntelliSense.
|
||||
*
|
||||
* Usage in .agents/your-agent.ts:
|
||||
* import { AgentDefinition, ToolName, ModelName } from './types/agent-definition'
|
||||
*
|
||||
* const definition: AgentDefinition = {
|
||||
* // ... your agent configuration with full type safety ...
|
||||
* }
|
||||
*
|
||||
* export default definition
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Agent Definition and Utility Types
|
||||
// ============================================================================
|
||||
|
||||
export interface AgentDefinition {
|
||||
/** Unique identifier for this agent. Must contain only lowercase letters, numbers, and hyphens, e.g. 'code-reviewer' */
|
||||
id: string
|
||||
|
||||
/** Version string (if not provided, will default to '0.0.1' and be bumped on each publish) */
|
||||
version?: string
|
||||
|
||||
/** Publisher ID for the agent. Must be provided if you want to publish the agent. */
|
||||
publisher?: string
|
||||
|
||||
/** Human-readable name for the agent */
|
||||
displayName: string
|
||||
|
||||
/** AI model to use for this agent. Can be any model in OpenRouter: https://openrouter.ai/models */
|
||||
model: ModelName
|
||||
|
||||
/**
|
||||
* https://openrouter.ai/docs/use-cases/reasoning-tokens
|
||||
* One of `max_tokens` or `effort` is required.
|
||||
* If `exclude` is true, reasoning will be removed from the response. Default is false.
|
||||
*/
|
||||
reasoningOptions?: {
|
||||
enabled?: boolean
|
||||
exclude?: boolean
|
||||
} & (
|
||||
| {
|
||||
max_tokens: number
|
||||
}
|
||||
| {
|
||||
effort: 'high' | 'medium' | 'low' | 'minimal' | 'none'
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Provider routing options for OpenRouter.
|
||||
* Controls which providers to use and fallback behavior.
|
||||
* See https://openrouter.ai/docs/features/provider-routing
|
||||
*/
|
||||
providerOptions?: {
|
||||
/**
|
||||
* List of provider slugs to try in order (e.g. ["anthropic", "openai"])
|
||||
*/
|
||||
order?: string[]
|
||||
/**
|
||||
* Whether to allow backup providers when primary is unavailable (default: true)
|
||||
*/
|
||||
allow_fallbacks?: boolean
|
||||
/**
|
||||
* Only use providers that support all parameters in your request (default: false)
|
||||
*/
|
||||
require_parameters?: boolean
|
||||
/**
|
||||
* Control whether to use providers that may store data
|
||||
*/
|
||||
data_collection?: 'allow' | 'deny'
|
||||
/**
|
||||
* List of provider slugs to allow for this request
|
||||
*/
|
||||
only?: string[]
|
||||
/**
|
||||
* List of provider slugs to skip for this request
|
||||
*/
|
||||
ignore?: string[]
|
||||
/**
|
||||
* List of quantization levels to filter by (e.g. ["int4", "int8"])
|
||||
*/
|
||||
quantizations?: Array<
|
||||
| 'int4'
|
||||
| 'int8'
|
||||
| 'fp4'
|
||||
| 'fp6'
|
||||
| 'fp8'
|
||||
| 'fp16'
|
||||
| 'bf16'
|
||||
| 'fp32'
|
||||
| 'unknown'
|
||||
>
|
||||
/**
|
||||
* Sort providers by price, throughput, or latency
|
||||
*/
|
||||
sort?: 'price' | 'throughput' | 'latency'
|
||||
/**
|
||||
* Maximum pricing you want to pay for this request
|
||||
*/
|
||||
max_price?: {
|
||||
prompt?: number | string
|
||||
completion?: number | string
|
||||
image?: number | string
|
||||
audio?: number | string
|
||||
request?: number | string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tools and Subagents
|
||||
// ============================================================================
|
||||
|
||||
/** MCP servers by name. Names cannot contain `/`. */
|
||||
mcpServers?: Record<string, MCPConfig>
|
||||
|
||||
/**
|
||||
* Tools this agent can use.
|
||||
*
|
||||
* By default, all tools are available from any specified MCP server. In
|
||||
* order to limit the tools from a specific MCP server, add the tool name(s)
|
||||
* in the format `'mcpServerName/toolName1'`, `'mcpServerName/toolName2'`,
|
||||
* etc.
|
||||
*/
|
||||
toolNames?: (ToolName | (string & {}))[]
|
||||
|
||||
/** Other agents this agent can spawn, like 'codebuff/file-picker@0.0.1'.
|
||||
*
|
||||
* Use the fully qualified agent id from the agent store, including publisher and version: 'codebuff/file-picker@0.0.1'
|
||||
* (publisher and version are required!)
|
||||
*
|
||||
* Or, use the agent id from a local agent file in your .agents directory: 'file-picker'.
|
||||
*/
|
||||
spawnableAgents?: string[]
|
||||
|
||||
// ============================================================================
|
||||
// Input and Output
|
||||
// ============================================================================
|
||||
|
||||
/** The input schema required to spawn the agent. Provide a prompt string and/or a params object or none.
|
||||
* 80% of the time you want just a prompt string with a description:
|
||||
* inputSchema: {
|
||||
* prompt: { type: 'string', description: 'A description of what info would be helpful to the agent' }
|
||||
* }
|
||||
*/
|
||||
inputSchema?: {
|
||||
prompt?: { type: 'string'; description?: string }
|
||||
params?: JsonObjectSchema
|
||||
}
|
||||
|
||||
/** How the agent should output a response to its parent (defaults to 'last_message')
|
||||
*
|
||||
* last_message: The last message from the agent, typically after using tools.
|
||||
*
|
||||
* all_messages: All messages from the agent, including tool calls and results.
|
||||
*
|
||||
* structured_output: Make the agent output a JSON object. Can be used with outputSchema or without if you want freeform json output.
|
||||
*/
|
||||
outputMode?: 'last_message' | 'all_messages' | 'structured_output'
|
||||
|
||||
/** JSON schema for structured output (when outputMode is 'structured_output') */
|
||||
outputSchema?: JsonObjectSchema
|
||||
|
||||
// ============================================================================
|
||||
// Prompts
|
||||
// ============================================================================
|
||||
|
||||
/** Prompt for when and why to spawn this agent. Include the main purpose and use cases.
|
||||
*
|
||||
* This field is key if the agent is intended to be spawned by other agents. */
|
||||
spawnerPrompt?: string
|
||||
|
||||
/** Whether to include conversation history from the parent agent in context.
|
||||
*
|
||||
* Defaults to false.
|
||||
* Use this when the agent needs to know all the previous messages in the conversation.
|
||||
*/
|
||||
includeMessageHistory?: boolean
|
||||
|
||||
/** Whether to inherit the parent agent's system prompt instead of using this agent's own systemPrompt.
|
||||
*
|
||||
* Defaults to false.
|
||||
* Use this when you want to enable prompt caching by preserving the same system prompt prefix.
|
||||
* Cannot be used together with the systemPrompt field.
|
||||
*/
|
||||
inheritParentSystemPrompt?: boolean
|
||||
|
||||
/** Background information for the agent. Fairly optional. Prefer using instructionsPrompt for agent instructions. */
|
||||
systemPrompt?: string
|
||||
|
||||
/** Instructions for the agent.
|
||||
*
|
||||
* IMPORTANT: Updating this prompt is the best way to shape the agent's behavior.
|
||||
* This prompt is inserted after each user input. */
|
||||
instructionsPrompt?: string
|
||||
|
||||
/** Prompt inserted at each agent step.
|
||||
*
|
||||
* Powerful for changing the agent's behavior, but usually not necessary for smart models.
|
||||
* Prefer instructionsPrompt for most instructions. */
|
||||
stepPrompt?: string
|
||||
|
||||
// ============================================================================
|
||||
// Handle Steps
|
||||
// ============================================================================
|
||||
|
||||
/** Programmatically step the agent forward and run tools.
|
||||
*
|
||||
* You can either yield:
|
||||
* - A tool call object with toolName and input properties.
|
||||
* - 'STEP' to run agent's model and generate one assistant message.
|
||||
* - 'STEP_ALL' to run the agent's model until it uses the end_turn tool or stops includes no tool calls in a message.
|
||||
*
|
||||
* Or use 'return' to end the turn.
|
||||
*
|
||||
* Example 1:
|
||||
* function* handleSteps({ agentState, prompt, params, logger }) {
|
||||
* logger.info('Starting file read process')
|
||||
* const { toolResult } = yield {
|
||||
* toolName: 'read_files',
|
||||
* input: { paths: ['file1.txt', 'file2.txt'] }
|
||||
* }
|
||||
* yield 'STEP_ALL'
|
||||
*
|
||||
* // Optionally do a post-processing step here...
|
||||
* logger.info('Files read successfully, setting output')
|
||||
* yield {
|
||||
* toolName: 'set_output',
|
||||
* input: {
|
||||
* output: 'The files were read successfully.',
|
||||
* },
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Example 2:
|
||||
* handleSteps: function* ({ agentState, prompt, params, logger }) {
|
||||
* while (true) {
|
||||
* logger.debug('Spawning thinker agent')
|
||||
* yield {
|
||||
* toolName: 'spawn_agents',
|
||||
* input: {
|
||||
* agents: [
|
||||
* {
|
||||
* agent_type: 'thinker',
|
||||
* prompt: 'Think deeply about the user request',
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
* }
|
||||
* const { stepsComplete } = yield 'STEP'
|
||||
* if (stepsComplete) break
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
handleSteps?: (context: AgentStepContext) => Generator<
|
||||
ToolCall | 'STEP' | 'STEP_ALL' | StepText | GenerateN,
|
||||
void,
|
||||
{
|
||||
agentState: AgentState
|
||||
toolResult: ToolResultOutput[] | undefined
|
||||
stepsComplete: boolean
|
||||
nResponses?: string[]
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Supporting Types
|
||||
// ============================================================================
|
||||
|
||||
export interface AgentState {
|
||||
agentId: string
|
||||
runId: string
|
||||
parentId: string | undefined
|
||||
|
||||
/** The agent's conversation history: messages from the user and the assistant. */
|
||||
messageHistory: Message[]
|
||||
|
||||
/** The last value set by the set_output tool. This is a plain object or undefined if not set. */
|
||||
output: Record<string, any> | undefined
|
||||
|
||||
/** The system prompt for this agent. */
|
||||
systemPrompt: string
|
||||
|
||||
/** The tool definitions for this agent. */
|
||||
toolDefinitions: Record<
|
||||
string,
|
||||
{ description: string | undefined; inputSchema: {} }
|
||||
>
|
||||
|
||||
/**
|
||||
* The token count from the Anthropic API.
|
||||
* This is updated on every agent step via the /api/v1/token-count endpoint.
|
||||
*/
|
||||
contextTokenCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Context provided to handleSteps generator function
|
||||
*/
|
||||
export interface AgentStepContext {
|
||||
agentState: AgentState
|
||||
prompt?: string
|
||||
params?: Record<string, any>
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type StepText = { type: 'STEP_TEXT'; text: string }
|
||||
export type GenerateN = { type: 'GENERATE_N'; n: number }
|
||||
|
||||
/**
|
||||
* Tool call object for handleSteps generator
|
||||
*/
|
||||
export type ToolCall<T extends ToolName = ToolName> = {
|
||||
[K in T]: {
|
||||
toolName: K
|
||||
input: GetToolParams<K>
|
||||
includeToolCall?: boolean
|
||||
}
|
||||
}[T]
|
||||
|
||||
// ============================================================================
|
||||
// Available Tools
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* File operation tools
|
||||
*/
|
||||
export type FileEditingTools = 'read_files' | 'write_file' | 'str_replace'
|
||||
|
||||
/**
|
||||
* Code analysis tools
|
||||
*/
|
||||
export type CodeAnalysisTools = 'code_search' | 'find_files' | 'read_files'
|
||||
|
||||
/**
|
||||
* Terminal and system tools
|
||||
*/
|
||||
export type TerminalTools = 'run_terminal_command' | 'code_search'
|
||||
|
||||
/**
|
||||
* Web and browser tools
|
||||
*/
|
||||
export type WebTools = 'web_search' | 'read_docs'
|
||||
|
||||
/**
|
||||
* Agent management tools
|
||||
*/
|
||||
export type AgentTools = 'spawn_agents'
|
||||
|
||||
/**
|
||||
* Output and control tools
|
||||
*/
|
||||
export type OutputTools = 'set_output'
|
||||
|
||||
// ============================================================================
|
||||
// Available Models (see: https://openrouter.ai/models)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI models available for agents. Pick from our selection of recommended models or choose any model in OpenRouter.
|
||||
*
|
||||
* See available models at https://openrouter.ai/models
|
||||
*/
|
||||
export type ModelName =
|
||||
// Recommended Models
|
||||
|
||||
// OpenAI
|
||||
| 'openai/gpt-5.1'
|
||||
| 'openai/gpt-5.1-chat'
|
||||
| 'openai/gpt-5-mini'
|
||||
| 'openai/gpt-5-nano'
|
||||
|
||||
// Anthropic
|
||||
| 'anthropic/claude-sonnet-4.5'
|
||||
| 'anthropic/claude-opus-4.1'
|
||||
|
||||
// Gemini
|
||||
| 'google/gemini-2.5-pro'
|
||||
| 'google/gemini-2.5-flash'
|
||||
| 'google/gemini-2.5-flash-lite'
|
||||
| 'google/gemini-2.5-flash-preview-09-2025'
|
||||
| 'google/gemini-2.5-flash-lite-preview-09-2025'
|
||||
|
||||
// X-AI
|
||||
| 'x-ai/grok-4-07-09'
|
||||
| 'x-ai/grok-4-fast'
|
||||
| 'x-ai/grok-code-fast-1'
|
||||
|
||||
// Qwen
|
||||
| 'qwen/qwen3-max'
|
||||
| 'qwen/qwen3-coder-plus'
|
||||
| 'qwen/qwen3-coder'
|
||||
| 'qwen/qwen3-coder:nitro'
|
||||
| 'qwen/qwen3-coder-flash'
|
||||
| 'qwen/qwen3-235b-a22b-2507'
|
||||
| 'qwen/qwen3-235b-a22b-2507:nitro'
|
||||
| 'qwen/qwen3-235b-a22b-thinking-2507'
|
||||
| 'qwen/qwen3-235b-a22b-thinking-2507:nitro'
|
||||
| 'qwen/qwen3-30b-a3b'
|
||||
| 'qwen/qwen3-30b-a3b:nitro'
|
||||
|
||||
// DeepSeek
|
||||
| 'deepseek/deepseek-chat-v3-0324'
|
||||
| 'deepseek/deepseek-chat-v3-0324:nitro'
|
||||
| 'deepseek/deepseek-r1-0528'
|
||||
| 'deepseek/deepseek-r1-0528:nitro'
|
||||
|
||||
// Other open source models
|
||||
| 'moonshotai/kimi-k2'
|
||||
| 'moonshotai/kimi-k2:nitro'
|
||||
| 'z-ai/glm-4.6'
|
||||
| 'z-ai/glm-4.6:nitro'
|
||||
| (string & {})
|
||||
|
||||
import type { ToolName, GetToolParams } from './tools'
|
||||
import type {
|
||||
Message,
|
||||
ToolResultOutput,
|
||||
JsonObjectSchema,
|
||||
MCPConfig,
|
||||
Logger,
|
||||
} from './util-types'
|
||||
|
||||
export type { ToolName, GetToolParams }
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Union type of all available tool names
|
||||
*/
|
||||
export type ToolName =
|
||||
| 'add_message'
|
||||
| 'ask_user'
|
||||
| 'code_search'
|
||||
| 'end_turn'
|
||||
| 'find_files'
|
||||
| 'glob'
|
||||
| 'list_directory'
|
||||
| 'lookup_agent_info'
|
||||
| 'read_docs'
|
||||
| 'read_files'
|
||||
| 'read_subtree'
|
||||
| 'run_file_change_hooks'
|
||||
| 'run_terminal_command'
|
||||
| 'set_messages'
|
||||
| 'set_output'
|
||||
| 'spawn_agents'
|
||||
| 'str_replace'
|
||||
| 'suggest_followups'
|
||||
| 'task_completed'
|
||||
| 'think_deeply'
|
||||
| 'web_search'
|
||||
| 'write_file'
|
||||
| 'write_todos'
|
||||
|
||||
/**
|
||||
* Map of tool names to their parameter types
|
||||
*/
|
||||
export interface ToolParamsMap {
|
||||
add_message: AddMessageParams
|
||||
ask_user: AskUserParams
|
||||
code_search: CodeSearchParams
|
||||
end_turn: EndTurnParams
|
||||
find_files: FindFilesParams
|
||||
glob: GlobParams
|
||||
list_directory: ListDirectoryParams
|
||||
lookup_agent_info: LookupAgentInfoParams
|
||||
read_docs: ReadDocsParams
|
||||
read_files: ReadFilesParams
|
||||
read_subtree: ReadSubtreeParams
|
||||
run_file_change_hooks: RunFileChangeHooksParams
|
||||
run_terminal_command: RunTerminalCommandParams
|
||||
set_messages: SetMessagesParams
|
||||
set_output: SetOutputParams
|
||||
spawn_agents: SpawnAgentsParams
|
||||
str_replace: StrReplaceParams
|
||||
suggest_followups: SuggestFollowupsParams
|
||||
task_completed: TaskCompletedParams
|
||||
think_deeply: ThinkDeeplyParams
|
||||
web_search: WebSearchParams
|
||||
write_file: WriteFileParams
|
||||
write_todos: WriteTodosParams
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new message to the conversation history. To be used for complex requests that can't be solved in a single step, as you may forget what happened!
|
||||
*/
|
||||
export interface AddMessageParams {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user multiple choice questions and pause execution until they respond.
|
||||
*/
|
||||
export interface AskUserParams {
|
||||
/** List of multiple choice questions to ask the user */
|
||||
questions: {
|
||||
/** The question to ask the user */
|
||||
question: string
|
||||
/** Short label (max 12 chars) displayed as a chip/tag */
|
||||
header?: string
|
||||
/** Array of answer options with label and optional description (minimum 2) */
|
||||
options: {
|
||||
/** The display text for this option */
|
||||
label: string
|
||||
/** Explanation shown when option is focused */
|
||||
description?: string
|
||||
}[]
|
||||
/** If true, allows selecting multiple options (checkbox). If false, single selection only (radio). */
|
||||
multiSelect?: boolean
|
||||
/** Validation rules for "Other" text input */
|
||||
validation?: {
|
||||
/** Maximum length for "Other" text input */
|
||||
maxLength?: number
|
||||
/** Minimum length for "Other" text input */
|
||||
minLength?: number
|
||||
/** Regex pattern for "Other" text input */
|
||||
pattern?: string
|
||||
/** Custom error message when pattern fails */
|
||||
patternError?: string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for string patterns in the project's files. This tool uses ripgrep (rg), a fast line-oriented search tool. Use this tool only when read_files is not sufficient to find the files you need.
|
||||
*/
|
||||
export interface CodeSearchParams {
|
||||
/** The pattern to search for. */
|
||||
pattern: string
|
||||
/** Optional ripgrep flags to customize the search (e.g., "-i" for case-insensitive, "-g *.ts -g *.js" for TypeScript and JavaScript files only, "-g !*.test.ts" to exclude Typescript test files, "-A 3" for 3 lines after match, "-B 2" for 2 lines before match). */
|
||||
flags?: string
|
||||
/** Optional working directory to search within, relative to the project root. Defaults to searching the entire project. */
|
||||
cwd?: string
|
||||
/** Maximum number of results to return per file. Defaults to 15. There is also a global limit of 250 results across all files. */
|
||||
maxResults?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* End your turn, regardless of any new tool results that might be coming. This will allow the user to type another prompt.
|
||||
*/
|
||||
export interface EndTurnParams {}
|
||||
|
||||
/**
|
||||
* Find several files related to a brief natural language description of the files or the name of a function or class you are looking for.
|
||||
*/
|
||||
export interface FindFilesParams {
|
||||
/** A brief natural language description of the files or the name of a function or class you are looking for. It's also helpful to mention a directory or two to look within. */
|
||||
prompt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for files matching a glob pattern. Returns matching file paths sorted by modification time.
|
||||
*/
|
||||
export interface GlobParams {
|
||||
/** Glob pattern to match files against (e.g., *.js, src/glob/*.ts, glob/test/glob/*.go). */
|
||||
pattern: string
|
||||
/** Optional working directory to search within, relative to project root. If not provided, searches from project root. */
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* List files and directories in the specified path. Returns separate arrays of file names and directory names.
|
||||
*/
|
||||
export interface ListDirectoryParams {
|
||||
/** Directory path to list, relative to the project root. */
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve information about an agent by ID
|
||||
*/
|
||||
export interface LookupAgentInfoParams {
|
||||
/** Agent ID (short local or full published format) */
|
||||
agentId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch up-to-date documentation for libraries and frameworks using Context7 API.
|
||||
*/
|
||||
export interface ReadDocsParams {
|
||||
/** The library or framework name (e.g., "Next.js", "MongoDB", "React"). Use the official name as it appears in documentation if possible. Only public libraries available in Context7's database are supported, so small or private libraries may not be available. */
|
||||
libraryTitle: string
|
||||
/** Specific topic to focus on (e.g., "routing", "hooks", "authentication") */
|
||||
topic: string
|
||||
/** Optional maximum number of tokens to return. Defaults to 20000. Values less than 10000 are automatically increased to 10000. */
|
||||
max_tokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the multiple files from disk and return their contents. Use this tool to read as many files as would be helpful to answer the user's request.
|
||||
*/
|
||||
export interface ReadFilesParams {
|
||||
/** List of file paths to read. */
|
||||
paths: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one or more directory subtrees (as a blob including subdirectories, file names, and parsed variables within each source file) or return parsed variable names for files. If no paths are provided, returns the entire project tree.
|
||||
*/
|
||||
export interface ReadSubtreeParams {
|
||||
/** List of paths to directories or files. Relative to the project root. If omitted, the entire project tree is used. */
|
||||
paths?: string[]
|
||||
/** Maximum token budget for the subtree blob; the tree will be truncated to fit within this budget by first dropping file variables and then removing the most-nested files and directories. */
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for run_file_change_hooks tool
|
||||
*/
|
||||
export interface RunFileChangeHooksParams {
|
||||
/** List of file paths that were changed and should trigger file change hooks */
|
||||
files: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a CLI command from the **project root** (different from the user's cwd).
|
||||
*/
|
||||
export interface RunTerminalCommandParams {
|
||||
/** CLI command valid for user's OS. */
|
||||
command: string
|
||||
/** Either SYNC (waits, returns output) or BACKGROUND (runs in background). Default SYNC */
|
||||
process_type?: 'SYNC' | 'BACKGROUND'
|
||||
/** The working directory to run the command in. Default is the project root. */
|
||||
cwd?: string
|
||||
/** Set to -1 for no timeout. Does not apply for BACKGROUND commands. Default 30 */
|
||||
timeout_seconds?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the conversation history to the provided messages.
|
||||
*/
|
||||
export interface SetMessagesParams {
|
||||
messages: any
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON object to set as the agent output. This completely replaces any previous output. If the agent was spawned, this value will be passed back to its parent. If the agent has an outputSchema defined, the output will be validated against it.
|
||||
*/
|
||||
export interface SetOutputParams {}
|
||||
|
||||
/**
|
||||
* Spawn multiple agents and send a prompt and/or parameters to each of them. These agents will run in parallel. Note that that means they will run independently. If you need to run agents sequentially, use spawn_agents with one agent at a time instead.
|
||||
*/
|
||||
export interface SpawnAgentsParams {
|
||||
agents: {
|
||||
/** Agent to spawn */
|
||||
agent_type: string
|
||||
/** Prompt to send to the agent */
|
||||
prompt?: string
|
||||
/** Parameters object for the agent (if any) */
|
||||
params?: Record<string, any>
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace strings in a file with new strings.
|
||||
*/
|
||||
export interface StrReplaceParams {
|
||||
/** The path to the file to edit. */
|
||||
path: string
|
||||
/** Array of replacements to make. */
|
||||
replacements: {
|
||||
/** The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation. */
|
||||
old: string
|
||||
/** The string to replace the corresponding old string with. Can be empty to delete. */
|
||||
new: string
|
||||
/** Whether to allow multiple replacements of old string. */
|
||||
allowMultiple?: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest clickable followup prompts to the user.
|
||||
*/
|
||||
export interface SuggestFollowupsParams {
|
||||
/** List of suggested followup prompts the user can click to send */
|
||||
followups: {
|
||||
/** The full prompt text to send as a user message when clicked */
|
||||
prompt: string
|
||||
/** Short display label for the card (defaults to truncated prompt if not provided) */
|
||||
label?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the task is complete. Use this tool when:
|
||||
- The user's request is completely fulfilled
|
||||
- You need clarification from the user before continuing
|
||||
- You are stuck or need help from the user to continue
|
||||
|
||||
This tool explicitly marks the end of your work on the current task.
|
||||
*/
|
||||
export interface TaskCompletedParams {}
|
||||
|
||||
/**
|
||||
* Deeply consider complex tasks by brainstorming approaches and tradeoffs step-by-step.
|
||||
*/
|
||||
export interface ThinkDeeplyParams {
|
||||
/** Detailed step-by-step analysis. Initially keep each step concise (max ~5-7 words per step). */
|
||||
thought: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the web for current information using Linkup API.
|
||||
*/
|
||||
export interface WebSearchParams {
|
||||
/** The search query to find relevant web content */
|
||||
query: string
|
||||
/** Search depth - 'standard' for quick results, 'deep' for more comprehensive search. Default is 'standard'. */
|
||||
depth?: 'standard' | 'deep'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or edit a file with the given content.
|
||||
*/
|
||||
export interface WriteFileParams {
|
||||
/** Path to the file relative to the **project root** */
|
||||
path: string
|
||||
/** What the change is intended to do in only one sentence. */
|
||||
instructions: string
|
||||
/** Edit snippet to apply to the file. */
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a todo list to track tasks for multi-step implementations. Use this frequently to maintain an updated step-by-step plan.
|
||||
*/
|
||||
export interface WriteTodosParams {
|
||||
/** List of todos with their completion status. Add ALL of the applicable tasks to the list, so you don't forget to do anything. Try to order the todos the same way you will complete them. Do not mark todos as completed if you have not completed them yet! */
|
||||
todos: {
|
||||
/** Description of the task */
|
||||
task: string
|
||||
/** Whether the task is completed */
|
||||
completed: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameters type for a specific tool
|
||||
*/
|
||||
export type GetToolParams<T extends ToolName> = ToolParamsMap[T]
|
||||
@@ -0,0 +1,175 @@
|
||||
// ===== JSON Types =====
|
||||
export type JSONValue =
|
||||
| null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| JSONObject
|
||||
| JSONArray
|
||||
|
||||
export type JSONObject = { [key: string]: JSONValue }
|
||||
|
||||
export type JSONArray = JSONValue[]
|
||||
|
||||
/**
|
||||
* JSON Schema definition (for prompt schema or output schema)
|
||||
*/
|
||||
export type JsonSchema = {
|
||||
type?:
|
||||
| 'object'
|
||||
| 'array'
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'null'
|
||||
| 'integer'
|
||||
description?: string
|
||||
properties?: Record<string, JsonSchema | boolean>
|
||||
required?: string[]
|
||||
enum?: Array<string | number | boolean | null>
|
||||
[k: string]: unknown
|
||||
}
|
||||
export type JsonObjectSchema = JsonSchema & { type: 'object' }
|
||||
|
||||
// ===== Data Content Types =====
|
||||
export type DataContent = string | Uint8Array | ArrayBuffer | Buffer
|
||||
|
||||
// ===== Provider Metadata Types =====
|
||||
export type ProviderMetadata = Record<string, Record<string, JSONValue>>
|
||||
|
||||
// ===== Content Part Types =====
|
||||
export type TextPart = {
|
||||
type: 'text'
|
||||
text: string
|
||||
providerOptions?: ProviderMetadata
|
||||
}
|
||||
|
||||
export type ImagePart = {
|
||||
type: 'image'
|
||||
image: DataContent
|
||||
mediaType?: string
|
||||
providerOptions?: ProviderMetadata
|
||||
}
|
||||
|
||||
export type FilePart = {
|
||||
type: 'file'
|
||||
data: DataContent
|
||||
filename?: string
|
||||
mediaType: string
|
||||
providerOptions?: ProviderMetadata
|
||||
}
|
||||
|
||||
export type ReasoningPart = {
|
||||
type: 'reasoning'
|
||||
text: string
|
||||
providerOptions?: ProviderMetadata
|
||||
}
|
||||
|
||||
export type ToolCallPart = {
|
||||
type: 'tool-call'
|
||||
toolCallId: string
|
||||
toolName: string
|
||||
input: Record<string, unknown>
|
||||
providerOptions?: ProviderMetadata
|
||||
providerExecuted?: boolean
|
||||
}
|
||||
|
||||
export type ToolResultOutput =
|
||||
| {
|
||||
type: 'json'
|
||||
value: JSONValue
|
||||
}
|
||||
| {
|
||||
type: 'media'
|
||||
data: string
|
||||
mediaType: string
|
||||
}
|
||||
|
||||
// ===== Message Types =====
|
||||
export type AuxiliaryMessageData = {
|
||||
providerOptions?: ProviderMetadata
|
||||
tags?: string[]
|
||||
|
||||
/** @deprecated Use tags instead. */
|
||||
timeToLive?: 'agentStep' | 'userPrompt'
|
||||
/** @deprecated Use tags instead. */
|
||||
keepDuringTruncation?: boolean
|
||||
/** @deprecated Use tags instead. */
|
||||
keepLastTags?: string[]
|
||||
}
|
||||
|
||||
export type SystemMessage = {
|
||||
role: 'system'
|
||||
content: TextPart[]
|
||||
} & AuxiliaryMessageData
|
||||
|
||||
export type UserMessage = {
|
||||
role: 'user'
|
||||
content: (TextPart | ImagePart | FilePart)[]
|
||||
} & AuxiliaryMessageData
|
||||
|
||||
export type AssistantMessage = {
|
||||
role: 'assistant'
|
||||
content: (TextPart | ReasoningPart | ToolCallPart)[]
|
||||
} & AuxiliaryMessageData
|
||||
|
||||
export type ToolMessage = {
|
||||
role: 'tool'
|
||||
toolCallId: string
|
||||
toolName: string
|
||||
content: ToolResultOutput[]
|
||||
} & AuxiliaryMessageData
|
||||
|
||||
export type Message =
|
||||
| SystemMessage
|
||||
| UserMessage
|
||||
| AssistantMessage
|
||||
| ToolMessage
|
||||
|
||||
// ===== MCP Server Types =====
|
||||
|
||||
/**
|
||||
* MCP server configuration for stdio-based servers.
|
||||
*
|
||||
* Environment variables in `env` can be:
|
||||
* - A plain string value (hardcoded, e.g., `'production'`)
|
||||
* - A `$VAR_NAME` reference to read from local environment (e.g., `'$NOTION_TOKEN'`)
|
||||
*
|
||||
* The `$VAR_NAME` syntax reads from `process.env.VAR_NAME` at agent load time.
|
||||
* This keeps secrets out of your agent definitions - store them in `.env.local` instead.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* env: {
|
||||
* // Read NOTION_TOKEN from local .env file
|
||||
* NOTION_TOKEN: '$NOTION_TOKEN',
|
||||
* // Read MY_API_KEY from local env, pass as API_KEY to MCP server
|
||||
* API_KEY: '$MY_API_KEY',
|
||||
* // Hardcoded value (non-secret)
|
||||
* NODE_ENV: 'production',
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type MCPConfig =
|
||||
| {
|
||||
type?: 'stdio'
|
||||
command: string
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
}
|
||||
| {
|
||||
type?: 'http' | 'sse'
|
||||
url: string
|
||||
params?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Logger Interface
|
||||
// ============================================================================
|
||||
export interface Logger {
|
||||
debug: (data: any, msg?: string) => void
|
||||
info: (data: any, msg?: string) => void
|
||||
warn: (data: any, msg?: string) => void
|
||||
error: (data: any, msg?: string) => void
|
||||
}
|
||||
@@ -13,7 +13,12 @@ websitedev/
|
||||
├── proto/ # Main Next.js application
|
||||
│ ├── src/
|
||||
│ │ ├── app/ # Next.js App Router (pages and API routes)
|
||||
│ │ ├── components/ # React components (Header, Footer)
|
||||
│ │ ├── components/ # React components (Header, Footer, ThemeProvider)
|
||||
│ │ ├── content/ # JSON content management system
|
||||
│ │ │ ├── types.ts # Content TypeScript definitions
|
||||
│ │ │ ├── index.ts # Content loader utility
|
||||
│ │ │ ├── common.json # Shared texts (buttons, labels)
|
||||
│ │ │ └── pages/ # Page-specific content JSONs
|
||||
│ │ ├── lib/ # Utility libraries (MongoDB, Logger, Site Config)
|
||||
│ │ ├── config/ # Static site configuration
|
||||
│ │ └── types/ # TypeScript type definitions
|
||||
@@ -28,6 +33,8 @@ websitedev/
|
||||
|
||||
- **Framework**: Next.js 14 with App Router and TypeScript
|
||||
- **UI**: Tailwind CSS 4 with custom fonts (Geist Sans/Mono)
|
||||
- **Design System**: CSS Variables, Dark Mode (ThemeProvider), Animations
|
||||
- **Content Management**: JSON-based structured content in `src/content/`
|
||||
- **Database**: MongoDB with Mongoose ODM
|
||||
- **Logging**: Winston with Loki integration
|
||||
- **Testing**: Jest with React Testing Library
|
||||
@@ -80,11 +87,43 @@ Required environment variables (check `.env` and `proto/.env.local`):
|
||||
## Site Configuration
|
||||
|
||||
The site uses a centralized configuration system:
|
||||
- `src/config/site.ts` - Main site configuration with all content
|
||||
- `src/config/site.ts` - Main site configuration (company info, navigation, services)
|
||||
- `src/types/site.ts` - TypeScript interfaces for configuration
|
||||
- `src/lib/site-config.ts` - Runtime configuration utilities
|
||||
|
||||
This approach enables easy content updates and future CMS integration.
|
||||
## Content Management System
|
||||
|
||||
All page text content is managed through JSON files for easy modification:
|
||||
|
||||
```
|
||||
src/content/
|
||||
├── types.ts # TypeScript definitions for content
|
||||
├── index.ts # Content loader with getPageContent() helper
|
||||
├── common.json # Shared texts (buttons, labels, validation)
|
||||
└── pages/
|
||||
├── home.json # Homepage CTA, feature labels
|
||||
├── about.json # About page (hero, story, mission, team)
|
||||
├── services.json # Services details, support info
|
||||
└── contact.json # Form labels, FAQ, contact info
|
||||
```
|
||||
|
||||
**Usage in components:**
|
||||
```typescript
|
||||
import { content, getPageContent } from '@/content'
|
||||
|
||||
const { about: pageContent } = content.pages
|
||||
// or
|
||||
const servicesContent = getPageContent('services')
|
||||
```
|
||||
|
||||
## Design System
|
||||
|
||||
The project includes a comprehensive design system:
|
||||
|
||||
- **CSS Variables**: Defined in `globals.css` for colors, shadows, transitions
|
||||
- **Dark Mode**: ThemeProvider component with system preference + manual toggle
|
||||
- **Animations**: fadeIn, float, pulse, hover effects (lift, scale, glow)
|
||||
- **Utility Classes**: `.card`, `.btn`, `.icon-container`, etc.
|
||||
|
||||
## Database Integration
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ This project uses **separated sync systems** for optimal management:
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Frontend**: Next.js 14, TypeScript, Tailwind CSS
|
||||
- **Frontend**: Next.js 14, TypeScript, Tailwind CSS 4
|
||||
- **Design System**: CSS Variables, Dark Mode, Custom Animations
|
||||
- **Content Management**: JSON-based structured content
|
||||
- **Backend**: Next.js API Routes, MongoDB
|
||||
- **Testing**: Jest, React Testing Library, Docker
|
||||
- **Deployment**: Dokploy, Docker
|
||||
@@ -59,6 +61,9 @@ This project uses **separated sync systems** for optimal management:
|
||||
|
||||
### ✅ Completed Features
|
||||
- [x] Responsive website with modern design
|
||||
- [x] **Design System** with CSS variables, animations, and micro-interactions
|
||||
- [x] **Dark Mode** with system preference detection and manual toggle
|
||||
- [x] **JSON Content Management** - Structured, easily modifiable content system
|
||||
- [x] Contact form with validation & spam protection
|
||||
- [x] Rate limiting and security features
|
||||
- [x] Docker development environment
|
||||
@@ -90,3 +95,37 @@ This project uses **separated sync systems** for optimal management:
|
||||
- [Traceability Matrix](./TRACEABILITY-MATRIX.md)
|
||||
- [Docker Setup](./DOCKER.md)
|
||||
- [Development Guide](./proto/README.md)
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
The project uses a comprehensive design system with:
|
||||
|
||||
- **CSS Variables**: Brand colors, spacing, shadows, transitions
|
||||
- **Dark Mode**: Automatic system preference + manual toggle via ThemeProvider
|
||||
- **Animations**: fadeIn, float, pulse, hover effects (lift, scale, glow)
|
||||
- **Components**: Buttons, cards, icons with consistent styling
|
||||
|
||||
## 📝 Content Management
|
||||
|
||||
All website text content is managed through JSON files:
|
||||
|
||||
```
|
||||
proto/src/content/
|
||||
├── types.ts # TypeScript definitions
|
||||
├── index.ts # Content loader utility
|
||||
├── common.json # Shared texts (buttons, labels)
|
||||
└── pages/
|
||||
├── home.json # Homepage content
|
||||
├── about.json # About page content
|
||||
├── services.json # Services page content
|
||||
└── contact.json # Contact page content
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { content, getPageContent } from '@/content'
|
||||
|
||||
// Access content
|
||||
const aboutContent = content.pages.about
|
||||
const servicesContent = getPageContent('services')
|
||||
```
|
||||
|
||||
@@ -17,6 +17,8 @@ Next.js 14 alapú weboldal a mozdIT Bt. számára, Dokploy-on hostolva.
|
||||
| ZEE-33 | Kapcsolat űrlap + API stub | ✅ |
|
||||
| ZEE-34 | /api/health endpoint | ✅ |
|
||||
| ZEE-35 | Dockerfile + Docker Compose fejlesztői környezet | ✅ |
|
||||
| ZEE-29 | Tailwind + alap layout (design system, dark mode, animációk) | ✅ |
|
||||
| - | JSON alapú content management rendszer (struktúrált szövegkezelés) | ✅ |
|
||||
| - | Unit teszt infrastruktúra beállítása (Jest + React Testing Library) | ✅ |
|
||||
| - | API endpoint tesztek írása (/api/health) | ✅ |
|
||||
| - | Component tesztek írása (Header, Footer) | ✅ |
|
||||
@@ -29,12 +31,12 @@ Next.js 14 alapú weboldal a mozdIT Bt. számára, Dokploy-on hostolva.
|
||||
## 🔄 Folyamatban (Linear szerint)
|
||||
| Linear Ticket | Feladat | Státusz |
|
||||
|---------------|---------|---------|
|
||||
| - | Jelenleg nincs aktív folyamatban lévő feladat | - |
|
||||
| - | ZEE-152 | - |
|
||||
|
||||
## 📋 Tervezett (Backlog)
|
||||
| Linear Ticket | Feladat | Státusz |
|
||||
|---------------|---------|---------|
|
||||
| ZEE-29 | Tailwind + alap layout | 📋 |
|
||||
|
||||
| ZEE-36 | CI (lint, unit) + Staging deploy trigger | 📋 |
|
||||
| ZEE-37 | Playwright smoke E2E + Lighthouse CI | 📋 |
|
||||
| ZEE-38 | Prod app + domain + HTTPS | 📋 |
|
||||
@@ -166,4 +168,4 @@ Minden feladat megtalálható a Linear-ben megfelelő ticket számmal. A TODO.md
|
||||
**Backlog ticketek:** ZEE-29, ZEE-36-46
|
||||
**Duplikált ticketek eltávolítva:** ZEE-27 (duplikáció a ZEE-28-hoz képest)
|
||||
|
||||
**Utolsó szinkronizáció:** 2025-09-05 - Docker fejlesztői környezet implementáció befejezése után
|
||||
**Utolsó szinkronizáció:** 2025-01-23 - ZEE-29 (Tailwind + layout) és JSON content management befejezése
|
||||
@@ -0,0 +1,171 @@
|
||||
# mozdIT Weboldal - Fejlesztési TODO Lista
|
||||
|
||||
## Projekt Áttekintés
|
||||
Next.js 14 alapú weboldal a mozdIT Bt. számára, Dokploy-on hostolva.
|
||||
|
||||
## ⚠️ Projekt Management
|
||||
**Elsődleges forrás**: Linear (ZEE-28, ZEE-29, ZEE-30, stb.)
|
||||
**Lokális másolat**: Ez a fájl csak referencia, a Linear az authoritative source
|
||||
|
||||
## ✅ Befejezett (Linear szerint)
|
||||
| Linear Ticket | Feladat | Státusz |
|
||||
|---------------|---------|---------|
|
||||
| ZEE-28 | Repo & Next.js bootstrap - befejezett, dev szerver fut localhost:3001-n | ✅ |
|
||||
| ZEE-30 | Kezdőlap (Hero + USP + Webmail CTA) | ✅ |
|
||||
| ZEE-31 | Rólunk oldal | ✅ |
|
||||
| ZEE-32 | Szolgáltatások oldal | ✅ |
|
||||
| ZEE-33 | Kapcsolat űrlap + API stub | ✅ |
|
||||
| ZEE-34 | /api/health endpoint | ✅ |
|
||||
| ZEE-35 | Dockerfile + Docker Compose fejlesztői környezet | ✅ |
|
||||
| ZEE-29 | Tailwind + alap layout (design system, dark mode, animációk) | ✅ |
|
||||
| - | JSON alapú content management rendszer (struktúrált szövegkezelés) | ✅ |
|
||||
| - | Unit teszt infrastruktúra beállítása (Jest + React Testing Library) | ✅ |
|
||||
| - | API endpoint tesztek írása (/api/health) | ✅ |
|
||||
| - | Component tesztek írása (Header, Footer) | ✅ |
|
||||
| - | Teszt hibák javítása (Jest setup, duplicate elements, type casting) | ✅ |
|
||||
| - | Mobil navigáció JavaScript funkcionalitás | ✅ |
|
||||
| - | Contact API endpoint (/api/contact) - rate limiting, spam detection, validáció | ✅ |
|
||||
| - | Docker fejlesztői környezet (Next.js + MongoDB + Mongo Express + Loki + Grafana) | ✅ |
|
||||
| - | MongoDB inicializálás és site_config beállítás | ✅ |
|
||||
|
||||
## 🔄 Folyamatban (Linear szerint)
|
||||
| Linear Ticket | Feladat | Státusz |
|
||||
|---------------|---------|---------|
|
||||
| - | Jelenleg nincs aktív folyamatban lévő feladat | - |
|
||||
|
||||
## 📋 Tervezett (Backlog)
|
||||
| Linear Ticket | Feladat | Státusz |
|
||||
|---------------|---------|---------|
|
||||
|
||||
| ZEE-36 | CI (lint, unit) + Staging deploy trigger | 📋 |
|
||||
| ZEE-37 | Playwright smoke E2E + Lighthouse CI | 📋 |
|
||||
| ZEE-38 | Prod app + domain + HTTPS | 📋 |
|
||||
| ZEE-39 | Site config migrálás MongoDB-ba | 📋 |
|
||||
| ZEE-40 | Winston logger + Loki integráció | 📋 |
|
||||
|
||||
## 📋 Követelmény Nyilvántartás (REQ Prefix)
|
||||
| Linear Ticket | Követelmény | Kategória | Státusz |
|
||||
|---------------|-------------|-----------|---------|
|
||||
| ZEE-50 | REQ-001: Kezdőlap Funkcionalitás | Funkcionális | ✅ |
|
||||
| ZEE-51 | REQ-004: Kapcsolat Űrlap | Funkcionális | ✅ |
|
||||
| ZEE-52 | REQ-101: Responsive Design | Nem-funkcionális | ✅ |
|
||||
| ZEE-53 | REQ-102: Accessibility (A11y) | Nem-funkcionális | 🔄 |
|
||||
| ZEE-54 | REQ-401: Lighthouse Score ≥ 90 | Teljesítmény | 🔄 |
|
||||
| ZEE-41 | Logging middleware megvalósítása | 📋 |
|
||||
| ZEE-42 | Grafana dashboard konfiguráció | 📋 |
|
||||
| ZEE-43 | SEO optimalizálás | 📋 |
|
||||
| ZEE-44 | Performance optimalizálás | 📋 |
|
||||
| ZEE-45 | Reszponzív design finomítása | 📋 |
|
||||
|
||||
## 🥒 Test Reporting Rendszer (Gherkin Format)
|
||||
| Funkció | Státusz | Leírás |
|
||||
|---------|---------|---------|
|
||||
| Gherkin generálás | ✅ | Automatikus TC-XXX prefix felismerés |
|
||||
| Funkcionális területek elemzése | ✅ | 8 kategória szerinti csoportosítás |
|
||||
| Coverage dashboard | ✅ | Területenkénti lemaradás elemzés |
|
||||
| TC issue frissítés | ✅ | Linear issues Gherkin formátummal |
|
||||
| Automatizált reporting | ✅ | GitHub Actions integráció |
|
||||
|
||||
### 🎯 Funkcionális Területek
|
||||
- **🏠 Weboldal Funkcionalitás**: Kezdőlap, navigáció, oldalak
|
||||
- **📝 Kapcsolat Űrlap**: Validáció, spam védelem, rate limiting
|
||||
- **🎨 Design és UX**: Responsive, accessibility, performance
|
||||
- **🔒 Biztonság**: Input validáció, rate limiting, HTTPS
|
||||
- **⚡ Teljesítmény**: Lighthouse, optimalizálás, caching
|
||||
- **🛠️ Technikai Stack**: Next.js, MongoDB, Docker, CI/CD
|
||||
|
||||
### 📊 Jelenlegi Test Coverage
|
||||
- **Összes teszt**: 56
|
||||
- **TC-XXX prefix**: 1 (TC-001)
|
||||
- **Funkcionális területek**: 1 (Kapcsolat Űrlap)
|
||||
- **Sikeres tesztek**: 100%
|
||||
- **Lemaradás**: Nincs
|
||||
| ZEE-46 | Analytics integráció | 📋 |
|
||||
|
||||
## Technikai Stack
|
||||
- **Frontend**: Next.js 14, TypeScript, Tailwind CSS
|
||||
- **Backend**: Next.js API Routes
|
||||
- **Database**: MongoDB
|
||||
- **Logging**: Winston + Loki
|
||||
- **Monitoring**: Grafana
|
||||
- **Deployment**: Dokploy
|
||||
- **Testing**: Jest, React Testing Library
|
||||
- **Project Management**: Linear (elsődleges)
|
||||
|
||||
## Fejlesztési Parancsok
|
||||
|
||||
### Hagyományos fejlesztés
|
||||
```bash
|
||||
# Fejlesztői szerver indítása
|
||||
cd proto && npm run dev
|
||||
|
||||
# Tesztek futtatása
|
||||
cd proto && npm test
|
||||
|
||||
# Build készítése
|
||||
cd proto && npm run build
|
||||
```
|
||||
|
||||
### Docker fejlesztői környezet
|
||||
```bash
|
||||
# Docker stack indítása (teljes környezet)
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Docker stack leállítása
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
|
||||
# Logok követése
|
||||
docker-compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
# Alkalmazás újraépítése
|
||||
docker-compose -f docker-compose.dev.yml up --build -d
|
||||
|
||||
# Teljes tisztítás (adatok törlése)
|
||||
docker-compose -f docker-compose.dev.yml down -v
|
||||
```
|
||||
|
||||
### Elérhető szolgáltatások (Docker)
|
||||
- **Weboldal**: http://localhost:3000
|
||||
- **MongoDB UI**: http://localhost:8081 (admin/password123)
|
||||
- **Grafana**: http://localhost:3001 (admin/admin123)
|
||||
- **Loki**: http://localhost:3100
|
||||
- **MongoDB**: mongodb://admin:password123@localhost:27017/admin
|
||||
|
||||
## Fontos Megjegyzés
|
||||
A Linear az authoritative project management rendszer. Ez a fájl csak lokális referencia, mindig ellenőrizd a Linear-t az aktuális státuszért és priorításokért.
|
||||
|
||||
## Frissítési Napló
|
||||
- **2025-09-05**: TODO.md fájl létrehozása, Linear integráció megjegyzésekkel
|
||||
- **2025-09-05**: Szinkronizáció Linear Website Development projekttel - aktuális státuszok frissítve
|
||||
- **2025-09-05**: Nagyobb implementációs mérföldkő - ZEE-31, ZEE-32, ZEE-33 befejezve, mobil navigáció és contact API implementálva
|
||||
- **2025-09-05**: Docker fejlesztői környezet implementálva - ZEE-34, ZEE-35 befejezve, teljes stack (Next.js + MongoDB + Monitoring) működik
|
||||
## Szinkronizálás Utmutató
|
||||
|
||||
A TODO.md és Linear között párhuzamos vezetéshez használd a `linear-sync.js` scriptet:
|
||||
|
||||
```bash
|
||||
# API kulcs hozzáadása a .env fájlhoz (Linear settings -> API)
|
||||
# Szerkesd a .env fájlt és add hozzá:
|
||||
# LINEAR_API_KEY=lin_api_your_actual_key_here
|
||||
|
||||
# Szinkronizálás futtatása
|
||||
node linear-sync.js
|
||||
|
||||
# vagy
|
||||
./linear-sync.js
|
||||
```
|
||||
|
||||
A script:
|
||||
- Megkeresi a TODO.md-ben hiányzó Linear ticket-eket
|
||||
- Létrehozza ezeket a Linear-ben
|
||||
- Frissíti a Linear ticket státuszokat a TODO alapján
|
||||
- Visszairja a Linear ticket számokat a TODO.md-be
|
||||
|
||||
### ✅ Szinkronizáció állapota:
|
||||
Minden feladat megtalálható a Linear-ben megfelelő ticket számmal. A TODO.md mostantól teljesen szinkronban van a Linear Website Development projekttel.
|
||||
|
||||
**Befejezett ticketek:** ZEE-28, ZEE-30, ZEE-31, ZEE-32, ZEE-33, ZEE-34, ZEE-35
|
||||
**Backlog ticketek:** ZEE-29, ZEE-36-46
|
||||
**Duplikált ticketek eltávolítva:** ZEE-27 (duplikáció a ZEE-28-hoz képest)
|
||||
|
||||
**Utolsó szinkronizáció:** 2025-01-23 - ZEE-29 (Tailwind + layout) és JSON content management befejezése
|
||||
@@ -0,0 +1,74 @@
|
||||
# Project Knowledge
|
||||
|
||||
mozdIT Bt. website - Next.js 14 app for a Hungarian IT services company (web hosting, email, DNS).
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
cd proto && npm install
|
||||
|
||||
# Dev server (with Turbopack)
|
||||
cd proto && npm run dev
|
||||
|
||||
# Docker dev environment (includes MongoDB, Grafana, Loki)
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Tests
|
||||
cd proto && npm test # Unit tests
|
||||
cd proto && npm run test:all # All tests (unit + browser + docker)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **proto/** - Main Next.js 14 application
|
||||
- `src/app/` - App Router pages and API routes
|
||||
- `src/components/` - React components (Header, Footer, ThemeProvider)
|
||||
- `src/content/` - **JSON content management system**
|
||||
- `types.ts` - Content TypeScript definitions
|
||||
- `index.ts` - Content loader utility
|
||||
- `common.json` - Shared texts (buttons, labels)
|
||||
- `pages/*.json` - Page-specific content
|
||||
- `src/lib/` - Utilities (MongoDB, Logger, Site Config)
|
||||
- `src/config/` - Static site configuration
|
||||
- `src/types/` - TypeScript definitions
|
||||
- **docker/** - MongoDB initialization scripts
|
||||
- **docs/** - Project documentation (Hungarian)
|
||||
- **scripts/** - Test sync and reporting scripts
|
||||
|
||||
Path alias: `@/*` → `./src/*`
|
||||
|
||||
## Commands (run from proto/)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Dev server with Turbopack |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run lint` | ESLint check |
|
||||
| `npm test` | Unit tests |
|
||||
| `npm run test:browser` | Browser integration tests |
|
||||
| `npm run test:integration` | Docker integration tests |
|
||||
| `npm run test:e2e` | End-to-end tests |
|
||||
| `npm run test:all` | All test suites |
|
||||
| `npm run docker:dev` | Start Docker stack |
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Language**: Hungarian content, English code/comments
|
||||
- **Stack**: Next.js 14 App Router, TypeScript, Tailwind CSS 4
|
||||
- **Design System**: CSS variables, dark mode, animations in `globals.css`
|
||||
- **Content**: JSON files in `src/content/` for all page texts
|
||||
- **Database**: MongoDB with Mongoose
|
||||
- **Logging**: Winston with Loki integration
|
||||
- **Testing**: Jest + React Testing Library
|
||||
- **ESLint**: next/core-web-vitals + next/typescript
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **All commands run from `proto/`** - The main app lives in the proto subdirectory
|
||||
- **Docker required for integration tests** - Start with `docker-compose -f docker-compose.dev.yml up -d`
|
||||
- **Dual tracking**: Linear (ZEE-* tickets) + TODO.md for task management
|
||||
- **Environment variables**: Need `MONGODB_URI`, `MONGODB_DB` for database connection
|
||||
- **Path mapping**: Use `@/` imports (e.g., `@/lib/mongodb`, `@/content`)
|
||||
- **Content changes**: Edit JSON files in `src/content/pages/` - rebuild container to see changes
|
||||
- **Dark mode**: Uses `data-theme` attribute on `<html>`, managed by ThemeProvider
|
||||
+115
-20
@@ -1,36 +1,131 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# mozdIT Bt. Website - Next.js Application
|
||||
|
||||
## Getting Started
|
||||
Modern Next.js 14 website for mozdIT Bt. - Hungarian IT services company.
|
||||
|
||||
First, run the development server:
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Development server (with Turbopack)
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Open [http://localhost:3000](http://localhost:3000) to view the site.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
## 📁 Project Structure
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── page.tsx # Homepage
|
||||
│ ├── rolunk/ # About page
|
||||
│ ├── szolgaltatasok/ # Services page
|
||||
│ ├── kapcsolat/ # Contact page
|
||||
│ ├── api/ # API routes
|
||||
│ └── globals.css # Design system & styles
|
||||
├── components/ # React components
|
||||
│ ├── Header.tsx # Navigation header
|
||||
│ ├── Footer.tsx # Site footer
|
||||
│ └── ThemeProvider.tsx # Dark mode provider
|
||||
├── content/ # JSON content management
|
||||
│ ├── types.ts # Content type definitions
|
||||
│ ├── index.ts # Content loader
|
||||
│ ├── common.json # Shared texts
|
||||
│ └── pages/ # Page-specific content
|
||||
├── config/ # Site configuration
|
||||
├── lib/ # Utilities
|
||||
└── types/ # TypeScript definitions
|
||||
```
|
||||
|
||||
## Learn More
|
||||
## 🎨 Design System
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
The project uses a comprehensive design system defined in `globals.css`:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
### CSS Variables
|
||||
- Brand colors (`--color-primary-*`)
|
||||
- Semantic colors (`--color-background`, `--color-foreground`)
|
||||
- Shadows, transitions, border radius
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
### Dark Mode
|
||||
- Automatic system preference detection
|
||||
- Manual toggle via ThemeProvider
|
||||
- Uses `data-theme="dark"` attribute
|
||||
|
||||
## Deploy on Vercel
|
||||
### Animations
|
||||
- `animate-fade-in-up` - Fade in with upward motion
|
||||
- `animate-float` - Floating effect
|
||||
- `animate-pulse-slow` - Slow pulsing
|
||||
- `hover-lift`, `hover-scale`, `hover-glow` - Hover effects
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
### Utility Classes
|
||||
- `.card` - Card component styling
|
||||
- `.btn`, `.btn-primary`, `.btn-secondary` - Button styles
|
||||
- `.icon-container` - Icon wrapper styling
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
## 📝 Content Management
|
||||
|
||||
All page content is managed through JSON files in `src/content/`:
|
||||
|
||||
```typescript
|
||||
import { content, getPageContent } from '@/content'
|
||||
|
||||
// Access specific page content
|
||||
const aboutContent = content.pages.about
|
||||
|
||||
// Or use the helper function
|
||||
const servicesContent = getPageContent('services')
|
||||
```
|
||||
|
||||
### Content Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `common.json` | Shared texts (buttons, labels, validation) |
|
||||
| `pages/home.json` | Homepage CTA section |
|
||||
| `pages/about.json` | About page (hero, story, mission, team, CTA) |
|
||||
| `pages/services.json` | Services (hero, details, support, CTA) |
|
||||
| `pages/contact.json` | Contact (form labels, FAQ, info) |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
npm test
|
||||
|
||||
# Watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Coverage report
|
||||
npm run test:coverage
|
||||
|
||||
# All test suites
|
||||
npm run test:all
|
||||
```
|
||||
|
||||
## 🐳 Docker Development
|
||||
|
||||
For full-stack development with MongoDB and monitoring:
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
Services:
|
||||
- **Website**: http://localhost:3000
|
||||
- **MongoDB UI**: http://localhost:8081
|
||||
- **Grafana**: http://localhost:3001
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs)
|
||||
- [Tailwind CSS](https://tailwindcss.com/docs)
|
||||
- [Project Documentation](../README.md)
|
||||
|
||||
+543
-13
@@ -1,26 +1,556 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ========================================
|
||||
mozdIT Bt. Design System
|
||||
======================================== */
|
||||
|
||||
/* Design Tokens - Light Theme */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
/* Brand Colors */
|
||||
--color-primary-50: #eff6ff;
|
||||
--color-primary-100: #dbeafe;
|
||||
--color-primary-200: #bfdbfe;
|
||||
--color-primary-300: #93c5fd;
|
||||
--color-primary-400: #60a5fa;
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-primary-600: #2563eb;
|
||||
--color-primary-700: #1d4ed8;
|
||||
--color-primary-800: #1e40af;
|
||||
--color-primary-900: #1e3a8a;
|
||||
--color-primary-950: #172554;
|
||||
|
||||
/* Accent Colors */
|
||||
--color-accent-50: #eef2ff;
|
||||
--color-accent-100: #e0e7ff;
|
||||
--color-accent-200: #c7d2fe;
|
||||
--color-accent-500: #6366f1;
|
||||
--color-accent-600: #4f46e5;
|
||||
|
||||
/* Success Colors */
|
||||
--color-success-50: #f0fdf4;
|
||||
--color-success-500: #22c55e;
|
||||
--color-success-600: #16a34a;
|
||||
|
||||
/* Warning Colors */
|
||||
--color-warning-50: #fffbeb;
|
||||
--color-warning-500: #f59e0b;
|
||||
|
||||
/* Error Colors */
|
||||
--color-error-50: #fef2f2;
|
||||
--color-error-500: #ef4444;
|
||||
--color-error-600: #dc2626;
|
||||
|
||||
/* Neutral Colors - Light Mode */
|
||||
--color-background: #ffffff;
|
||||
--color-background-secondary: #f9fafb;
|
||||
--color-background-tertiary: #f3f4f6;
|
||||
--color-foreground: #111827;
|
||||
--color-foreground-secondary: #374151;
|
||||
--color-foreground-muted: #6b7280;
|
||||
--color-border: #e5e7eb;
|
||||
--color-border-light: #f3f4f6;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
/* Spacing */
|
||||
--container-padding: 1rem;
|
||||
--section-spacing: 4rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: var(--font-geist-sans), system-ui, -apple-system, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
/* Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--color-background: #0f172a;
|
||||
--color-background-secondary: #1e293b;
|
||||
--color-background-tertiary: #334155;
|
||||
--color-foreground: #f8fafc;
|
||||
--color-foreground-secondary: #e2e8f0;
|
||||
--color-foreground-muted: #94a3b8;
|
||||
--color-border: #334155;
|
||||
--color-border-light: #1e293b;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.4);
|
||||
}
|
||||
|
||||
/* System preference fallback */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root:not([data-theme="light"]) {
|
||||
--color-background: #0f172a;
|
||||
--color-background-secondary: #1e293b;
|
||||
--color-background-tertiary: #334155;
|
||||
--color-foreground: #f8fafc;
|
||||
--color-foreground-secondary: #e2e8f0;
|
||||
--color-foreground-muted: #94a3b8;
|
||||
--color-border: #334155;
|
||||
--color-border-light: #1e293b;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tailwind Theme Extension */
|
||||
@theme inline {
|
||||
--color-background: var(--color-background);
|
||||
--color-foreground: var(--color-foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-sans);
|
||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: var(--color-primary-500);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Focus Styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-foreground-muted);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Utility Classes
|
||||
======================================== */
|
||||
|
||||
/* Gradient Backgrounds */
|
||||
.bg-gradient-hero {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-primary-50) 0%,
|
||||
var(--color-accent-50) 50%,
|
||||
var(--color-primary-100) 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .bg-gradient-hero {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-background-secondary) 0%,
|
||||
var(--color-background-tertiary) 50%,
|
||||
var(--color-background-secondary) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Glass Effect */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .glass {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Animation Keyframes
|
||||
======================================== */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-slow) ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-down {
|
||||
animation: fadeInDown 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-bounce-slow {
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Staggered animations for children */
|
||||
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
||||
.stagger-children > *:nth-child(2) { animation-delay: 100ms; }
|
||||
.stagger-children > *:nth-child(3) { animation-delay: 200ms; }
|
||||
.stagger-children > *:nth-child(4) { animation-delay: 300ms; }
|
||||
.stagger-children > *:nth-child(5) { animation-delay: 400ms; }
|
||||
.stagger-children > *:nth-child(6) { animation-delay: 500ms; }
|
||||
|
||||
/* Hover Animations */
|
||||
.hover-lift {
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.hover-scale {
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.hover-scale:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.hover-glow {
|
||||
transition: box-shadow var(--transition-normal);
|
||||
}
|
||||
|
||||
.hover-glow:hover {
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-normal);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary-600);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-700);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-primary-600);
|
||||
border: 2px solid var(--color-primary-600);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-primary-50);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary:hover {
|
||||
background: var(--color-primary-900);
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.card {
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 2rem;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--color-primary-200);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card:hover {
|
||||
border-color: var(--color-primary-800);
|
||||
}
|
||||
|
||||
/* Icon Container */
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: var(--color-primary-100);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.icon-container-lg {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .icon-container {
|
||||
background: var(--color-primary-900);
|
||||
}
|
||||
|
||||
.group:hover .icon-container {
|
||||
background: var(--color-primary-200);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .group:hover .icon-container {
|
||||
background: var(--color-primary-800);
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.section {
|
||||
padding: var(--section-spacing) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--container-padding);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--container-padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
:root {
|
||||
--container-padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Link Styles */
|
||||
.link {
|
||||
color: var(--color-primary-600);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.link-underline::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--color-primary-600);
|
||||
transition: width var(--transition-normal);
|
||||
}
|
||||
|
||||
.link-underline:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Prose adjustments for dark mode */
|
||||
[data-theme="dark"] .prose {
|
||||
--tw-prose-body: var(--color-foreground-secondary);
|
||||
--tw-prose-headings: var(--color-foreground);
|
||||
--tw-prose-links: var(--color-primary-400);
|
||||
--tw-prose-bold: var(--color-foreground);
|
||||
}
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { content } from '@/content'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
const { contact: pageContent } = content.pages
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Kapcsolat | ${siteConfig.general.name}`,
|
||||
description: 'Vegye fel velünk a kapcsolatot! Segítünk minden IT kérdésében. Email, telefon és online űrlap is rendelkezésére áll.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.description,
|
||||
openGraph: {
|
||||
title: `Kapcsolat | ${siteConfig.general.name}`,
|
||||
description: 'Vegye fel velünk a kapcsolatot! Segítünk minden IT kérdésében.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.description,
|
||||
url: `${siteConfig.general.url}/kapcsolat`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { content } from '@/content'
|
||||
import { useState } from 'react'
|
||||
|
||||
const { contact: pageContent } = content.pages
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
@@ -20,27 +23,27 @@ export default function ContactPage() {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'A név megadása kötelező'
|
||||
newErrors.name = pageContent.form.fields.name.error
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Az email cím megadása kötelező'
|
||||
newErrors.email = pageContent.form.fields.email.errorRequired
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Érvénytelen email cím formátum'
|
||||
newErrors.email = pageContent.form.fields.email.errorInvalid
|
||||
}
|
||||
|
||||
if (!formData.subject.trim()) {
|
||||
newErrors.subject = 'A tárgy megadása kötelező'
|
||||
newErrors.subject = pageContent.form.fields.subject.error
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = 'Az üzenet megadása kötelező'
|
||||
newErrors.message = pageContent.form.fields.message.errorRequired
|
||||
} else if (formData.message.trim().length < 10) {
|
||||
newErrors.message = 'Az üzenet legalább 10 karakter hosszú legyen'
|
||||
newErrors.message = pageContent.form.fields.message.errorMinLength
|
||||
}
|
||||
|
||||
if (!formData.gdprConsent) {
|
||||
newErrors.gdprConsent = 'Az adatkezelési tájékoztató elfogadása kötelező'
|
||||
newErrors.gdprConsent = pageContent.form.fields.gdpr.error
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
@@ -93,7 +96,6 @@ export default function ContactPage() {
|
||||
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
|
||||
}))
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({ ...prev, [name]: '' }))
|
||||
}
|
||||
@@ -105,10 +107,10 @@ export default function ContactPage() {
|
||||
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
Kapcsolat
|
||||
{pageContent.hero.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
Vegye fel velünk a kapcsolatot! Szívesen segítünk minden IT kérdésében.
|
||||
{pageContent.hero.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -119,13 +121,13 @@ export default function ContactPage() {
|
||||
<div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Küldjön üzenetet
|
||||
{pageContent.form.title}
|
||||
</h2>
|
||||
|
||||
{submitStatus === 'success' && (
|
||||
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<p className="text-green-800">
|
||||
✅ Köszönjük üzenetét! Hamarosan felvesszük Önnel a kapcsolatot.
|
||||
✅ {pageContent.form.successMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -133,7 +135,7 @@ export default function ContactPage() {
|
||||
{submitStatus === 'error' && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-red-800">
|
||||
❌ Hiba történt az üzenet küldése során. Kérjük, próbálja újra vagy írjon közvetlenül a {siteConfig.contact.email} címre.
|
||||
❌ {pageContent.form.errorMessage.replace('{email}', siteConfig.contact.email)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -141,7 +143,7 @@ export default function ContactPage() {
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Név *
|
||||
{pageContent.form.fields.name.label} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -152,14 +154,14 @@ export default function ContactPage() {
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.name ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="Az Ön neve"
|
||||
placeholder={pageContent.form.fields.name.placeholder}
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red-600">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email cím *
|
||||
{pageContent.form.fields.email.label} *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -170,14 +172,14 @@ export default function ContactPage() {
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.email ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="pelda@email.hu"
|
||||
placeholder={pageContent.form.fields.email.placeholder}
|
||||
/>
|
||||
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tárgy *
|
||||
{pageContent.form.fields.subject.label} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -188,14 +190,14 @@ export default function ContactPage() {
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.subject ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="Miben segíthetünk?"
|
||||
placeholder={pageContent.form.fields.subject.placeholder}
|
||||
/>
|
||||
{errors.subject && <p className="mt-1 text-sm text-red-600">{errors.subject}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Üzenet *
|
||||
{pageContent.form.fields.message.label} *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
@@ -206,7 +208,7 @@ export default function ContactPage() {
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.message ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="Írja le részletesen kérését vagy kérdését..."
|
||||
placeholder={pageContent.form.fields.message.placeholder}
|
||||
/>
|
||||
{errors.message && <p className="mt-1 text-sm text-red-600">{errors.message}</p>}
|
||||
</div>
|
||||
@@ -220,9 +222,10 @@ export default function ContactPage() {
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
Elfogadom az <a href="/adatkezelesi-tajekoztato" className="text-blue-600 hover:text-blue-700 underline">adatkezelési tájékoztatót</a> és hozzájárulok személyes adataim kezeléséhez a kapcsolatfelvétel céljából. *
|
||||
</span>
|
||||
<span
|
||||
className="text-sm text-gray-700"
|
||||
dangerouslySetInnerHTML={{ __html: pageContent.form.fields.gdpr.label + ' *' }}
|
||||
/>
|
||||
</label>
|
||||
{errors.gdprConsent && <p className="mt-1 text-sm text-red-600">{errors.gdprConsent}</p>}
|
||||
</div>
|
||||
@@ -232,7 +235,7 @@ export default function ContactPage() {
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
{isSubmitting ? 'Küldés...' : 'Üzenet küldése'}
|
||||
{isSubmitting ? pageContent.form.submittingButton : pageContent.form.submitButton}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -242,7 +245,7 @@ export default function ContactPage() {
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Elérhetőségek
|
||||
{pageContent.info.title}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -251,7 +254,7 @@ export default function ContactPage() {
|
||||
<span className="text-lg">✉️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">Email</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.email.title}</h3>
|
||||
<a
|
||||
href={`mailto:${siteConfig.contact.email}`}
|
||||
className="text-blue-600 hover:text-blue-700"
|
||||
@@ -259,7 +262,7 @@ export default function ContactPage() {
|
||||
{siteConfig.contact.email}
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
24 órán belül válaszolunk
|
||||
{pageContent.info.email.responseTime}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +272,7 @@ export default function ContactPage() {
|
||||
<span className="text-lg">🏢</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">Cég</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.company.title}</h3>
|
||||
<p className="text-gray-700">{siteConfig.general.name}</p>
|
||||
<p className="text-sm text-gray-600">{siteConfig.contact.address}</p>
|
||||
</div>
|
||||
@@ -280,17 +283,17 @@ export default function ContactPage() {
|
||||
<span className="text-lg">🌐</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">Webmail hozzáférés</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.webmail.title}</h3>
|
||||
<a
|
||||
href={siteConfig.hero.cta.primary.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
Webmail belépés →
|
||||
{pageContent.info.webmail.linkText}
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Ügyfeleink számára
|
||||
{pageContent.info.webmail.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -300,30 +303,16 @@ export default function ContactPage() {
|
||||
{/* FAQ */}
|
||||
<div className="bg-gray-50 rounded-xl p-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-6">
|
||||
Gyakori kérdések
|
||||
{pageContent.faq.title}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Milyen gyorsan válaszolnak?</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Email üzenetekre 24 órán belül, sürgős esetekben telefonon is elérhetők vagyunk.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Van ingyenes konzultáció?</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Igen! Az első konzultáció mindig ingyenes, hogy megismerjük az Ön igényeit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Milyen fizetési módokat fogadnak el?</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Banki átutalás, PayPal és kártyás fizetés is lehetséges.
|
||||
</p>
|
||||
</div>
|
||||
{pageContent.faq.items.map((item, index) => (
|
||||
<div key={index}>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">{item.question}</h3>
|
||||
<p className="text-gray-600 text-sm">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "../components/Header";
|
||||
import Footer from "../components/Footer";
|
||||
import { ThemeProvider } from "../components/ThemeProvider";
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -61,15 +62,34 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="hu">
|
||||
<html lang="hu" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else if (savedTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
|
||||
style={{ background: 'var(--color-background)', color: 'var(--color-foreground)' }}
|
||||
>
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<ThemeProvider>
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+198
-66
@@ -1,25 +1,54 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { content } from '@/content'
|
||||
|
||||
const { home: pageContent } = content.pages
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<div className="space-y-0">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
|
||||
<section className="bg-gradient-hero py-20 lg:py-28 relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="absolute -top-40 -right-40 w-80 h-80 rounded-full opacity-30 animate-pulse-slow"
|
||||
style={{ background: 'var(--color-primary-200)' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-40 -left-40 w-96 h-96 rounded-full opacity-20 animate-pulse-slow"
|
||||
style={{ background: 'var(--color-accent-200)', animationDelay: '1s' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<h1
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight animate-fade-in-up"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{siteConfig.hero.title}
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-gray-600 max-w-4xl mx-auto mb-8 leading-relaxed">
|
||||
<p
|
||||
className="text-lg md:text-xl max-w-4xl mx-auto mb-10 leading-relaxed animate-fade-in-up"
|
||||
style={{ color: 'var(--color-foreground-muted)', animationDelay: '100ms' }}
|
||||
>
|
||||
{siteConfig.hero.description}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<div
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center animate-fade-in-up"
|
||||
style={{ animationDelay: '200ms' }}
|
||||
>
|
||||
<a
|
||||
href={siteConfig.hero.cta.primary.href}
|
||||
target={siteConfig.hero.cta.primary.external ? '_blank' : undefined}
|
||||
rel={siteConfig.hero.cta.primary.external ? 'noopener noreferrer' : undefined}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-4 rounded-md transition-colors text-lg inline-flex items-center gap-2"
|
||||
className="btn btn-primary text-lg px-8 py-4 group"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg
|
||||
className="w-5 h-5 transition-transform duration-200 group-hover:scale-110"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10" />
|
||||
</svg>
|
||||
{siteConfig.hero.cta.primary.text}
|
||||
@@ -27,7 +56,7 @@ export default function Home() {
|
||||
{siteConfig.hero.cta.secondary && (
|
||||
<a
|
||||
href={siteConfig.hero.cta.secondary.href}
|
||||
className="border-2 border-blue-600 text-blue-600 hover:bg-blue-50 font-medium px-8 py-4 rounded-md transition-colors text-lg"
|
||||
className="btn btn-secondary text-lg px-8 py-4"
|
||||
>
|
||||
{siteConfig.hero.cta.secondary.text}
|
||||
</a>
|
||||
@@ -37,19 +66,44 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
{/* USP Section */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-8">
|
||||
{siteConfig.about.title}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{siteConfig.about.usps.map((usp) => (
|
||||
<div key={usp.id} className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 group-hover:bg-blue-200 rounded-full flex items-center justify-center mx-auto mb-4 transition-colors">
|
||||
<section
|
||||
className="py-20"
|
||||
style={{ background: 'var(--color-background-secondary)' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2
|
||||
className="text-3xl md:text-4xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{siteConfig.about.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 stagger-children">
|
||||
{siteConfig.about.usps.map((usp, index) => (
|
||||
<div
|
||||
key={usp.id}
|
||||
className="group text-center p-6 rounded-xl transition-all duration-300 hover-lift animate-fade-in-up"
|
||||
style={{
|
||||
background: 'var(--color-background)',
|
||||
border: '1px solid var(--color-border)',
|
||||
animationDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="icon-container icon-container-lg mx-auto mb-4"
|
||||
>
|
||||
<span className="text-2xl">{usp.icon}</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">{usp.title}</h3>
|
||||
<p className="text-gray-600">{usp.description}</p>
|
||||
<h3
|
||||
className="text-lg font-semibold mb-2 transition-colors duration-200"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{usp.title}
|
||||
</h3>
|
||||
<p style={{ color: 'var(--color-foreground-muted)' }}>
|
||||
{usp.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -57,64 +111,142 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
{/* Services Section */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
|
||||
{siteConfig.services.title}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||
{siteConfig.services.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{siteConfig.services.services.map((service) => (
|
||||
<div key={service.id} className="group bg-white p-8 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-all duration-300 hover:border-blue-200">
|
||||
<div className="w-12 h-12 bg-blue-100 group-hover:bg-blue-200 rounded-lg flex items-center justify-center mb-4 transition-colors">
|
||||
<span className="text-xl">{service.icon}</span>
|
||||
<section
|
||||
className="py-20"
|
||||
style={{ background: 'var(--color-background)' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2
|
||||
className="text-3xl md:text-4xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{siteConfig.services.title}
|
||||
</h2>
|
||||
<p
|
||||
className="text-lg max-w-3xl mx-auto"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{siteConfig.services.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{siteConfig.services.services.map((service, index) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className="group card hover-lift"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<div className="icon-container mb-4">
|
||||
<span className="text-xl">{service.icon}</span>
|
||||
</div>
|
||||
<h3
|
||||
className="text-xl font-semibold mb-3 transition-colors duration-200 group-hover:text-blue-600"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{service.title}
|
||||
</h3>
|
||||
<p
|
||||
className="leading-relaxed mb-4"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{service.description}
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<h4
|
||||
className="text-sm font-semibold mb-2"
|
||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||
>
|
||||
{pageContent.serviceFeatures.title}
|
||||
</h4>
|
||||
<ul
|
||||
className="text-sm space-y-1.5"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{service.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start group/item">
|
||||
<span
|
||||
className="mr-2 transition-transform duration-200 group-hover/item:scale-125"
|
||||
style={{ color: 'var(--color-success-500)' }}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<a
|
||||
href="/kapcsolat"
|
||||
className="inline-flex items-center font-medium transition-all duration-200 group/link"
|
||||
style={{ color: 'var(--color-primary-600)' }}
|
||||
>
|
||||
{service.ctaText}
|
||||
<svg
|
||||
className="w-4 h-4 ml-1 transition-transform duration-200 group-hover/link:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3 group-hover:text-blue-600 transition-colors">{service.title}</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-2">Szolgáltatás jellemzők:</h4>
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
{service.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/kapcsolat" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium mt-4 transition-colors">
|
||||
{service.ctaText}
|
||||
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-gray-900 text-white py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Kapcsolatfelvétel az első lépés
|
||||
<section
|
||||
className="py-20 relative overflow-hidden"
|
||||
style={{ background: 'var(--color-foreground)' }}
|
||||
>
|
||||
{/* Animated background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-10">
|
||||
<div
|
||||
className="absolute top-10 left-10 w-32 h-32 rounded-full animate-float"
|
||||
style={{ background: 'var(--color-primary-500)' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-10 right-20 w-24 h-24 rounded-full animate-float"
|
||||
style={{ background: 'var(--color-accent-500)', animationDelay: '0.5s' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-1/2 right-1/4 w-16 h-16 rounded-full animate-float"
|
||||
style={{ background: 'var(--color-primary-400)', animationDelay: '1s' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<h2
|
||||
className="text-3xl md:text-4xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-background)' }}
|
||||
>
|
||||
{pageContent.cta.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 mb-8">
|
||||
Mutassuk meg, hogyan segíthetünk Önnek megvalósítani címeit!
|
||||
<p
|
||||
className="text-xl mb-8 max-w-2xl mx-auto"
|
||||
style={{ color: 'var(--color-background)', opacity: 0.8 }}
|
||||
>
|
||||
{pageContent.cta.subtitle}
|
||||
</p>
|
||||
<a
|
||||
href="/kapcsolat"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
|
||||
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
||||
>
|
||||
Kapcsolatfelvétel
|
||||
{pageContent.cta.button}
|
||||
<svg
|
||||
className="w-5 h-5 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+140
-102
@@ -1,153 +1,191 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { content } from '@/content'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
const { about: pageContent } = content.pages
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Rólunk | ${siteConfig.general.name}`,
|
||||
description: 'Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit. Több mint 10 éve nyújtunk megbízható IT szolgáltatásokat.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.description,
|
||||
openGraph: {
|
||||
title: `Rólunk | ${siteConfig.general.name}`,
|
||||
description: 'Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.ogDescription,
|
||||
url: `${siteConfig.general.url}/rolunk`,
|
||||
},
|
||||
}
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="space-y-16 py-8">
|
||||
<div className="space-y-0">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
Rólunk
|
||||
<section className="bg-gradient-hero py-16 lg:py-24 relative overflow-hidden">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="absolute -top-20 -right-20 w-60 h-60 rounded-full opacity-20 animate-pulse-slow"
|
||||
style={{ background: 'var(--color-primary-300)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<h1
|
||||
className="text-4xl md:text-5xl font-bold mb-6 animate-fade-in-up"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{pageContent.hero.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
Több mint 10 éve biztosítunk megbízható IT infrastruktúrát és személyes ügyfélszolgálatot
|
||||
<p
|
||||
className="text-xl leading-relaxed animate-fade-in-up"
|
||||
style={{ color: 'var(--color-foreground-muted)', animationDelay: '100ms' }}
|
||||
>
|
||||
{pageContent.hero.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Story Section */}
|
||||
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="prose prose-lg mx-auto">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-6">Történetünk</h2>
|
||||
<section
|
||||
className="py-16"
|
||||
style={{ background: 'var(--color-background)' }}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="prose prose-lg mx-auto">
|
||||
<h2
|
||||
className="text-3xl font-bold mb-6"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{pageContent.story.title}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 text-gray-700 leading-relaxed">
|
||||
<p>
|
||||
A <strong>mozdIT Bt.</strong> 2010-ben alakult azzal a céllal, hogy kisvállalkozások és
|
||||
magánszemélyek számára nyújtson megbízható, személyes IT szolgáltatásokat.
|
||||
Alapítóink több évtizedes tapasztalattal rendelkeznek a rendszeradminisztráció
|
||||
és webfejlesztés területén.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Kezdetben néhány ügyfél weboldalának üzemeltetésével indultunk, ma pedig
|
||||
több száz domain és email fiók működését biztosítjuk. Növekedésünk során
|
||||
mindig szem előtt tartottuk az alapelveinket: <em>megbízhatóság, személyes
|
||||
kapcsolat és műszaki kiválóság</em>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Csapatunk folyamatosan képezi magát a legújabb technológiák terén, hogy
|
||||
ügyfeleink mindig korszerű és biztonságos megoldásokat kapjanak. Büszkék
|
||||
vagyunk arra, hogy sok ügyfelünkkel évek óta tartjuk a kapcsolatot, és
|
||||
számos projektet vittünk sikerre közösen.
|
||||
</p>
|
||||
<div
|
||||
className="space-y-6 leading-relaxed"
|
||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||
>
|
||||
{pageContent.story.paragraphs.map((paragraph, index) => (
|
||||
<p
|
||||
key={index}
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission & Values */}
|
||||
<section className="bg-gray-50 py-16">
|
||||
<section
|
||||
className="py-16"
|
||||
style={{ background: 'var(--color-background-secondary)' }}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Küldetésünk és értékeink
|
||||
<h2
|
||||
className="text-3xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{pageContent.mission.title}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||
Minden nap azért dolgozunk, hogy ügyfeleink digitális jelenléte biztonságos,
|
||||
stabil és hatékony legyen.
|
||||
<p
|
||||
className="text-lg max-w-3xl mx-auto"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{pageContent.mission.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-2xl">🛡️</span>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 stagger-children">
|
||||
{pageContent.mission.values.map((item, index) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="group text-center p-8 rounded-xl hover-lift animate-fade-in-up"
|
||||
style={{
|
||||
background: 'var(--color-background)',
|
||||
border: '1px solid var(--color-border)',
|
||||
animationDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
<div className="icon-container icon-container-lg mx-auto mb-4">
|
||||
<span className="text-2xl">{item.icon}</span>
|
||||
</div>
|
||||
<h3
|
||||
className="text-xl font-semibold mb-3"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p style={{ color: 'var(--color-foreground-muted)' }}>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Megbízhatóság</h3>
|
||||
<p className="text-gray-600">
|
||||
99.9% uptime és 24/7 monitoring biztosítja, hogy szolgáltatásaink mindig
|
||||
elérhetők legyenek.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-2xl">👥</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Személyes kapcsolat</h3>
|
||||
<p className="text-gray-600">
|
||||
Minden ügyfél számít számunkra. Személyre szabott megoldásokat kínálunk
|
||||
és mindig elérhetők vagyunk.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-2xl">⚡</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Műszaki kiválóság</h3>
|
||||
<p className="text-gray-600">
|
||||
Korszerű technológiák és bevált gyakorlatok alkalmazásával biztosítjuk
|
||||
a legmagasabb színvonalú szolgáltatást.
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Team Section */}
|
||||
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Szakértő csapat
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Tapasztalt IT szakemberek, akik szenvedélyesen dolgoznak az ügyfeleink sikeréért
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
||||
<div className="prose prose-lg mx-auto">
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Csapatunk rendszeradminisztrátorokból, webfejlesztőkből és ügyfélszolgálati
|
||||
szakértőkből áll. Mindannyian több mint 10 éves tapasztalattal rendelkeznek
|
||||
a maguk területén, és folyamatosan követik a technológiai újdonságokat.
|
||||
<section
|
||||
className="py-16"
|
||||
style={{ background: 'var(--color-background)' }}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2
|
||||
className="text-3xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
{pageContent.team.title}
|
||||
</h2>
|
||||
<p
|
||||
className="text-lg"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{pageContent.team.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Hiszünk abban, hogy a jó kommunikáció és a műszaki tudás együtt teremti meg
|
||||
a tökéletes ügyfélélményt. Ezért minden munkatársunk nemcsak technikai
|
||||
szakértő, hanem kiváló kommunikátor is.
|
||||
</p>
|
||||
<div className="card">
|
||||
<div className="prose prose-lg mx-auto">
|
||||
{pageContent.team.paragraphs.map((paragraph, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className="leading-relaxed"
|
||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-gray-900 text-white py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Legyen Ön is elégedett ügyfelünk!
|
||||
<section
|
||||
className="py-16 relative overflow-hidden"
|
||||
style={{ background: 'var(--color-foreground)' }}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-10">
|
||||
<div
|
||||
className="absolute top-10 right-20 w-24 h-24 rounded-full animate-float"
|
||||
style={{ background: 'var(--color-primary-500)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<h2
|
||||
className="text-3xl font-bold mb-4"
|
||||
style={{ color: 'var(--color-background)' }}
|
||||
>
|
||||
{pageContent.cta.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 mb-8">
|
||||
Vegye fel velünk a kapcsolatot, és beszéljük meg, hogyan segíthetünk Önnek.
|
||||
<p
|
||||
className="text-xl mb-8"
|
||||
style={{ color: 'var(--color-background)', opacity: 0.8 }}
|
||||
>
|
||||
{pageContent.cta.subtitle}
|
||||
</p>
|
||||
<a
|
||||
href="/kapcsolat"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
|
||||
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
||||
>
|
||||
Kapcsolatfelvétel
|
||||
{pageContent.cta.button}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { content } from '@/content'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
const { services: pageContent } = content.pages
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Szolgáltatások | ${siteConfig.general.name}`,
|
||||
description: 'Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten. Ismerje meg részletes szolgáltatásainkat.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.description,
|
||||
openGraph: {
|
||||
title: `Szolgáltatások | ${siteConfig.general.name}`,
|
||||
description: 'Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten.',
|
||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
||||
description: pageContent.meta.ogDescription,
|
||||
url: `${siteConfig.general.url}/szolgaltatasok`,
|
||||
},
|
||||
}
|
||||
@@ -18,10 +21,10 @@ export default function ServicesPage() {
|
||||
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
Szolgáltatásaink
|
||||
{pageContent.hero.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
Teljes körű IT megoldások kisvállalkozások és magánszemélyek számára
|
||||
{pageContent.hero.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -39,7 +42,7 @@ export default function ServicesPage() {
|
||||
<p className="text-gray-600 leading-relaxed mb-6">{service.description}</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">Szolgáltatás jellemzők:</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">{content.common.labels.features}</h3>
|
||||
<ul className="space-y-2">
|
||||
{service.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
@@ -69,97 +72,35 @@ export default function ServicesPage() {
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Részletes szolgáltatásleírás
|
||||
{pageContent.details.title}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Minden szolgáltatásunk mögött évtizedes tapasztalat és modern technológia áll
|
||||
{pageContent.details.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
{/* Web Hosting Details */}
|
||||
<div className="bg-white rounded-xl p-8 shadow-sm">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xl">🌐</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">Web Hosting részletesen</h3>
|
||||
<div className="prose prose-lg text-gray-700">
|
||||
<p>
|
||||
Weboldalak biztonságos és gyors üzemeltetése SSD tárolással, automatikus biztonsági mentéssel
|
||||
és 24/7 monitoringgal. Támogatjuk a PHP, Python, Node.js technológiákat és MySQL/PostgreSQL
|
||||
adatbázisokat.
|
||||
</p>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">Technikai specifikációk:</h4>
|
||||
<ul className="space-y-1">
|
||||
<li>SSD tárhely 10GB-tól 500GB-ig</li>
|
||||
<li>Havi adatforgalom: korlátlan</li>
|
||||
<li>SSL tanúsítványok (Let's Encrypt vagy prémium)</li>
|
||||
<li>CDN integráció a gyorsabb betöltésért</li>
|
||||
<li>Automatikus napi biztonsági mentés</li>
|
||||
<li>cPanel vagy egyedi admin felület</li>
|
||||
</ul>
|
||||
{pageContent.details.services.map((service) => (
|
||||
<div key={service.title} className="bg-white rounded-xl p-8 shadow-sm">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xl">{service.icon}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{service.title}</h3>
|
||||
<div className="prose prose-lg text-gray-700">
|
||||
<p>{service.description}</p>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">{service.specs.title}</h4>
|
||||
<ul className="space-y-1">
|
||||
{service.specs.items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Service Details */}
|
||||
<div className="bg-white rounded-xl p-8 shadow-sm">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xl">✉️</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">Email szolgáltatás részletesen</h3>
|
||||
<div className="prose prose-lg text-gray-700">
|
||||
<p>
|
||||
Professzionális email fiókok saját domain névvel, spam szűréssel és vírusvédelemmel.
|
||||
Webmail felület és IMAP/POP3/SMTP támogatás minden népszerű email klienssel.
|
||||
</p>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">Email funkciók:</h4>
|
||||
<ul className="space-y-1">
|
||||
<li>Korlátlan email fiókok létrehozása</li>
|
||||
<li>5GB-50GB tárhelyet fiókként</li>
|
||||
<li>Webmail hozzáférés (Roundcube/SOGo)</li>
|
||||
<li>Mobilalkalmazás szinkronizáció</li>
|
||||
<li>Spam és vírusszűrés</li>
|
||||
<li>Email továbbítás és automatikus válaszok</li>
|
||||
<li>Backup és archiválás</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DNS Administration Details */}
|
||||
<div className="bg-white rounded-xl p-8 shadow-sm">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xl">⚙️</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">DNS adminisztráció részletesen</h3>
|
||||
<div className="prose prose-lg text-gray-700">
|
||||
<p>
|
||||
Teljes DNS kezelés domain regisztrációval, átvitellel és professzionális beállításokkal.
|
||||
Gyors propagáció és megbízható névszerverek világszerte.
|
||||
</p>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">DNS szolgáltatások:</h4>
|
||||
<ul className="space-y-1">
|
||||
<li>Domain regisztráció (.hu, .com, .eu, stb.)</li>
|
||||
<li>Domain átvitel más szolgáltatótól</li>
|
||||
<li>DNS rekord kezelés (A, CNAME, MX, TXT)</li>
|
||||
<li>Subdomain beállítások</li>
|
||||
<li>Redirect és forwarding szolgáltatások</li>
|
||||
<li>DNSSEC támogatás</li>
|
||||
<li>API hozzáférés fejlesztőknek</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -168,24 +109,18 @@ export default function ServicesPage() {
|
||||
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-blue-50 rounded-xl p-8 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Műszaki támogatás
|
||||
{pageContent.support.title}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
Minden szolgáltatásunkhoz teljes körű műszaki támogatást biztosítunk
|
||||
{pageContent.support.subtitle}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Email támogatás</h3>
|
||||
<p className="text-gray-600">24 órán belüli válasz</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Telefonos segítség</h3>
|
||||
<p className="text-gray-600">Munkaidőben elérhető</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Sürgős esetek</h3>
|
||||
<p className="text-gray-600">Azonnali beavatkozás</p>
|
||||
</div>
|
||||
{pageContent.support.channels.map((channel) => (
|
||||
<div key={channel.title}>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">{channel.title}</h3>
|
||||
<p className="text-gray-600">{channel.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -194,17 +129,17 @@ export default function ServicesPage() {
|
||||
<section className="bg-gray-900 text-white py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Kezdjük el a közös munkát!
|
||||
{pageContent.cta.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 mb-8">
|
||||
Vegye fel velünk a kapcsolatot ingyenes konzultációért és egyedi ajánlatért.
|
||||
{pageContent.cta.subtitle}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a
|
||||
href="/kapcsolat"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
|
||||
>
|
||||
Kapcsolatfelvétel
|
||||
{pageContent.cta.primaryButton}
|
||||
</a>
|
||||
<a
|
||||
href={siteConfig.hero.cta.primary.href}
|
||||
@@ -212,7 +147,7 @@ export default function ServicesPage() {
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block border-2 border-white text-white hover:bg-white hover:text-gray-900 font-medium px-8 py-3 rounded-md transition-colors"
|
||||
>
|
||||
Webmail belépés
|
||||
{pageContent.cta.secondaryButton}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+123
-24
@@ -1,36 +1,92 @@
|
||||
'use client'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-gray-50 border-t border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<footer
|
||||
className="border-t"
|
||||
style={{
|
||||
background: 'var(--color-background-secondary)',
|
||||
borderColor: 'var(--color-border)'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 lg:gap-12">
|
||||
{/* Company Info */}
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-xl font-bold mb-4 transition-colors duration-200"
|
||||
style={{ color: 'var(--color-primary-600)' }}
|
||||
>
|
||||
{siteConfig.general.name}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4 max-w-md">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full animate-pulse-slow"
|
||||
style={{ background: 'var(--color-success-500)' }}
|
||||
/>
|
||||
</a>
|
||||
<p
|
||||
className="mb-6 max-w-md leading-relaxed"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{siteConfig.general.description}
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>Email: {siteConfig.contact.email}</p>
|
||||
<p>Cég: {siteConfig.general.name}</p>
|
||||
<p>Székhely: {siteConfig.contact.address}</p>
|
||||
<div className="space-y-2 text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
|
||||
<a
|
||||
href={`mailto:${siteConfig.contact.email}`}
|
||||
className="flex items-center gap-2 group transition-colors duration-200 hover:text-blue-600"
|
||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||
>
|
||||
<span
|
||||
className="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-200 group-hover:scale-110"
|
||||
style={{ background: 'var(--color-primary-100)' }}
|
||||
>
|
||||
✉️
|
||||
</span>
|
||||
<span>{siteConfig.contact.email}</span>
|
||||
</a>
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||
>
|
||||
<span
|
||||
className="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center"
|
||||
style={{ background: 'var(--color-primary-100)' }}
|
||||
>
|
||||
🏢
|
||||
</span>
|
||||
<span>{siteConfig.contact.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4">
|
||||
<h3
|
||||
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
Navigáció
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-3">
|
||||
{siteConfig.navigation.footer.map((item) => (
|
||||
<li key={item.href}>
|
||||
<a href={item.href} className="text-gray-600 hover:text-blue-600 transition-colors">
|
||||
<a
|
||||
href={item.href}
|
||||
className="group flex items-center gap-2 text-sm transition-all duration-200"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = 'var(--color-primary-600)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = 'var(--color-foreground-muted)'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full transition-all duration-200 group-hover:scale-150"
|
||||
style={{ background: 'var(--color-primary-500)' }}
|
||||
/>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
@@ -40,32 +96,59 @@ export default function Footer() {
|
||||
|
||||
{/* Services */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4">
|
||||
<h3
|
||||
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
||||
style={{ color: 'var(--color-foreground)' }}
|
||||
>
|
||||
Szolgáltatások
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-3">
|
||||
{siteConfig.services.services.map((service) => (
|
||||
<li key={service.id} className="text-gray-600">
|
||||
<li
|
||||
key={service.id}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
<span className="text-base">{service.icon}</span>
|
||||
{service.title}
|
||||
</li>
|
||||
))}
|
||||
<li className="text-gray-600">Műszaki támogatás</li>
|
||||
<li
|
||||
className="flex items-center gap-2 text-sm"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
<span className="text-base">🛠️</span>
|
||||
Műszaki támogatás
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="border-t border-gray-200 pt-8 mt-8">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
<div
|
||||
className="border-t pt-8 mt-8"
|
||||
style={{ borderColor: 'var(--color-border)' }}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
>
|
||||
{siteConfig.footer.copyright}
|
||||
</p>
|
||||
<div className="flex space-x-4 mt-4 sm:mt-0">
|
||||
<div className="flex items-center gap-6">
|
||||
{siteConfig.footer.links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-gray-500 hover:text-blue-600 text-sm transition-colors"
|
||||
className="text-sm transition-all duration-200 link-underline relative"
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = 'var(--color-primary-600)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = 'var(--color-foreground-muted)'
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
@@ -73,7 +156,23 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tech badge */}
|
||||
<div className="mt-8 flex justify-center">
|
||||
<div
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs"
|
||||
style={{
|
||||
background: 'var(--color-background-tertiary)',
|
||||
color: 'var(--color-foreground-muted)'
|
||||
}}
|
||||
>
|
||||
<span>Powered by</span>
|
||||
<span className="font-medium" style={{ color: 'var(--color-foreground)' }}>Next.js</span>
|
||||
<span>•</span>
|
||||
<span className="font-medium" style={{ color: 'var(--color-foreground)' }}>Tailwind CSS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
)
|
||||
}
|
||||
+157
-44
@@ -1,93 +1,206 @@
|
||||
'use client'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ThemeToggle } from './ThemeProvider'
|
||||
|
||||
export default function Header() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 10)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen(!isMenuOpen)
|
||||
}
|
||||
|
||||
// Close menu on escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setIsMenuOpen(false)
|
||||
}
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => document.removeEventListener('keydown', handleEscape)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
||||
<header
|
||||
className={`sticky top-0 z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'glass shadow-md'
|
||||
: ''
|
||||
}`}
|
||||
style={{
|
||||
background: isScrolled ? undefined : 'var(--color-background)',
|
||||
borderBottom: `1px solid var(--color-border)`,
|
||||
}}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex-shrink-0">
|
||||
<a href="/" className="text-xl font-bold text-blue-600 hover:text-blue-700">
|
||||
{siteConfig.general.name}
|
||||
<a
|
||||
href="/"
|
||||
className="group flex items-center gap-2 text-xl font-bold transition-all duration-200"
|
||||
style={{ color: 'var(--color-primary-600)' }}
|
||||
>
|
||||
<span className="relative">
|
||||
<span className="group-hover:opacity-0 transition-opacity duration-200">
|
||||
{siteConfig.general.name}
|
||||
</span>
|
||||
<span
|
||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
style={{ color: 'var(--color-primary-700)' }}
|
||||
>
|
||||
{siteConfig.general.name}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full animate-pulse-slow"
|
||||
style={{ background: 'var(--color-success-500)' }}
|
||||
title="Online"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-center space-x-8">
|
||||
{siteConfig.navigation.main.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
className={item.label === 'Kapcsolat'
|
||||
? "bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
: "text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
|
||||
}
|
||||
>
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{siteConfig.navigation.main.map((item, index) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
className={`relative px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
item.label === 'Kapcsolat'
|
||||
? 'btn btn-primary text-white'
|
||||
: 'hover-scale'
|
||||
}`}
|
||||
style={item.label !== 'Kapcsolat' ? {
|
||||
color: 'var(--color-foreground)',
|
||||
} : undefined}
|
||||
>
|
||||
{item.label !== 'Kapcsolat' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg opacity-0 hover:opacity-100 transition-opacity duration-200"
|
||||
style={{ background: 'var(--color-primary-50)' }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
{item.external && (
|
||||
<svg className="inline-block w-3 h-3 ml-1 -mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="ml-2 pl-2" style={{ borderLeft: `1px solid var(--color-border)` }}>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<div className="md:hidden flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMenu}
|
||||
className="text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
className="p-2 rounded-lg transition-all duration-200"
|
||||
style={{
|
||||
color: 'var(--color-foreground-muted)',
|
||||
background: isMenuOpen ? 'var(--color-background-tertiary)' : 'transparent'
|
||||
}}
|
||||
aria-expanded={isMenuOpen}
|
||||
>
|
||||
<span className="sr-only">{isMenuOpen ? 'Close main menu' : 'Open main menu'}</span>
|
||||
{isMenuOpen ? (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
)}
|
||||
<div className="relative w-6 h-6">
|
||||
{/* Hamburger to X animation */}
|
||||
<span
|
||||
className={`absolute h-0.5 w-6 rounded-full transform transition-all duration-300 ${
|
||||
isMenuOpen ? 'rotate-45 top-3' : 'rotate-0 top-1'
|
||||
}`}
|
||||
style={{ background: 'currentColor' }}
|
||||
/>
|
||||
<span
|
||||
className={`absolute h-0.5 w-6 rounded-full top-3 transition-all duration-200 ${
|
||||
isMenuOpen ? 'opacity-0 translate-x-2' : 'opacity-100'
|
||||
}`}
|
||||
style={{ background: 'currentColor' }}
|
||||
/>
|
||||
<span
|
||||
className={`absolute h-0.5 w-6 rounded-full transform transition-all duration-300 ${
|
||||
isMenuOpen ? '-rotate-45 top-3' : 'rotate-0 top-5'
|
||||
}`}
|
||||
style={{ background: 'currentColor' }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation - Dynamic visibility */}
|
||||
<div className={`md:hidden absolute top-full left-0 right-0 bg-white border-b border-gray-200 shadow-lg transition-all duration-300 ease-in-out ${
|
||||
isMenuOpen
|
||||
? 'opacity-100 visible'
|
||||
: 'opacity-0 invisible'
|
||||
}`}>
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{siteConfig.navigation.main.map((item) => (
|
||||
{/* Mobile Navigation */}
|
||||
<div
|
||||
className={`md:hidden overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
isMenuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="py-3 space-y-1 border-t"
|
||||
style={{ borderColor: 'var(--color-border)' }}
|
||||
>
|
||||
{siteConfig.navigation.main.map((item, index) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className={item.label === 'Kapcsolat'
|
||||
? "block px-3 py-2 rounded-md text-base font-medium bg-blue-600 text-white"
|
||||
: "block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:text-blue-600 hover:bg-blue-50"
|
||||
}
|
||||
className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
|
||||
item.label === 'Kapcsolat' ? 'btn btn-primary text-white mt-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
...(item.label !== 'Kapcsolat' ? {
|
||||
color: 'var(--color-foreground)',
|
||||
background: 'transparent',
|
||||
} : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (item.label !== 'Kapcsolat') {
|
||||
e.currentTarget.style.background = 'var(--color-primary-50)'
|
||||
e.currentTarget.style.color = 'var(--color-primary-600)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (item.label !== 'Kapcsolat') {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = 'var(--color-foreground)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
<span className="flex items-center justify-between">
|
||||
{item.label}
|
||||
{item.external && (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
resolvedTheme: 'light' | 'dark'
|
||||
setTheme: (theme: Theme) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
// Return default values when not in provider (SSR/prerendering)
|
||||
return {
|
||||
theme: 'system' as const,
|
||||
resolvedTheme: 'light' as const,
|
||||
setTheme: () => {},
|
||||
toggleTheme: () => {},
|
||||
}
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode
|
||||
defaultTheme?: Theme
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = useState<Theme>(defaultTheme)
|
||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
const savedTheme = localStorage.getItem('theme') as Theme | null
|
||||
if (savedTheme) {
|
||||
setThemeState(savedTheme)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
const root = document.documentElement
|
||||
let resolved: 'light' | 'dark'
|
||||
|
||||
if (theme === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
root.removeAttribute('data-theme')
|
||||
} else {
|
||||
resolved = theme
|
||||
root.setAttribute('data-theme', theme)
|
||||
}
|
||||
|
||||
setResolvedTheme(resolved)
|
||||
}, [theme, mounted])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
if (theme === 'system') {
|
||||
setResolvedTheme(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [theme, mounted])
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme)
|
||||
localStorage.setItem('theme', newTheme)
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = resolvedTheme === 'light' ? 'dark' : 'light'
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
// Prevent hydration mismatch - render children without context until mounted
|
||||
if (!mounted) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Theme toggle button component
|
||||
export function ThemeToggle() {
|
||||
const { resolvedTheme, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="relative p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all duration-200 group"
|
||||
style={{
|
||||
background: resolvedTheme === 'dark' ? 'var(--color-background-tertiary)' : 'var(--color-background-tertiary)',
|
||||
}}
|
||||
aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{/* Sun icon */}
|
||||
<svg
|
||||
className={`w-5 h-5 transition-all duration-300 ${
|
||||
resolvedTheme === 'dark'
|
||||
? 'opacity-0 rotate-90 scale-0'
|
||||
: 'opacity-100 rotate-0 scale-100'
|
||||
}`}
|
||||
style={{ color: 'var(--color-foreground)', position: resolvedTheme === 'dark' ? 'absolute' : 'relative' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/* Moon icon */}
|
||||
<svg
|
||||
className={`w-5 h-5 transition-all duration-300 ${
|
||||
resolvedTheme === 'dark'
|
||||
? 'opacity-100 rotate-0 scale-100'
|
||||
: 'opacity-0 -rotate-90 scale-0'
|
||||
}`}
|
||||
style={{ color: 'var(--color-foreground)', position: resolvedTheme === 'light' ? 'absolute' : 'relative' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"buttons": {
|
||||
"contact": "Kapcsolatfelvétel",
|
||||
"learnMore": "További információk",
|
||||
"webmail": "Webmail belépés",
|
||||
"sendMessage": "Üzenet küldése"
|
||||
},
|
||||
"labels": {
|
||||
"required": "*",
|
||||
"features": "Szolgáltatás jellemzők:"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Ez a mező kötelező",
|
||||
"invalidEmail": "Érvénytelen email cím formátum",
|
||||
"minLength": "Legalább {min} karakter szükséges"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Content Loader - Centralized content management
|
||||
*
|
||||
* This module provides type-safe access to all JSON content files.
|
||||
* Content can be easily modified by editing the JSON files without code changes.
|
||||
*
|
||||
* Usage:
|
||||
* import { content, getPageContent } from '@/content'
|
||||
*
|
||||
* // Access specific page content
|
||||
* const aboutContent = content.pages.about
|
||||
*
|
||||
* // Or use the helper function
|
||||
* const servicesContent = getPageContent('services')
|
||||
*/
|
||||
|
||||
import type {
|
||||
SiteContent,
|
||||
CommonContent,
|
||||
HomePageContent,
|
||||
AboutPageContent,
|
||||
ServicesPageContent,
|
||||
ContactPageContent
|
||||
} from './types'
|
||||
|
||||
// Import JSON files
|
||||
import commonJson from './common.json'
|
||||
import homeJson from './pages/home.json'
|
||||
import aboutJson from './pages/about.json'
|
||||
import servicesJson from './pages/services.json'
|
||||
import contactJson from './pages/contact.json'
|
||||
|
||||
// Type assertions for JSON imports
|
||||
const common: CommonContent = commonJson as CommonContent
|
||||
const home: HomePageContent = homeJson as HomePageContent
|
||||
const about: AboutPageContent = aboutJson as AboutPageContent
|
||||
const services: ServicesPageContent = servicesJson as ServicesPageContent
|
||||
const contact: ContactPageContent = contactJson as ContactPageContent
|
||||
|
||||
// Combined content object
|
||||
export const content: SiteContent = {
|
||||
common,
|
||||
pages: {
|
||||
home,
|
||||
about,
|
||||
services,
|
||||
contact
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get page content with type safety
|
||||
export function getPageContent<T extends keyof SiteContent['pages']>(
|
||||
page: T
|
||||
): SiteContent['pages'][T] {
|
||||
return content.pages[page]
|
||||
}
|
||||
|
||||
// Export individual content for direct imports
|
||||
export { common, home, about, services, contact }
|
||||
|
||||
// Re-export types
|
||||
export * from './types'
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Rólunk",
|
||||
"description": "Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit. Több mint 10 éve nyújtunk megbízható IT szolgáltatásokat.",
|
||||
"ogDescription": "Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Rólunk",
|
||||
"subtitle": "Több mint 10 éve biztosítunk megbízható IT infrastruktúrát és személyes ügyfélszolgálatot"
|
||||
},
|
||||
"story": {
|
||||
"title": "Történetünk",
|
||||
"paragraphs": [
|
||||
"A <strong>mozdIT Bt.</strong> 2010-ben alakult azzal a céllal, hogy kisvállalkozások és magánszemélyek számára nyújtson megbízható, személyes IT szolgáltatásokat. Alapítóink több évtizedes tapasztalattal rendelkeznek a rendszeradminisztráció és webfejlesztés területén.",
|
||||
"Kezdetben néhány ügyfél weboldalának üzemeltetésével indultunk, ma pedig több száz domain és email fiók működését biztosítjuk. Növekedésünk során mindig szem előtt tartottuk az alapelveinket: <em>megbízhatóság, személyes kapcsolat és műszaki kiválóság</em>.",
|
||||
"Csapatunk folyamatosan képezi magát a legújabb technológiák terén, hogy ügyfeleink mindig korszerű és biztonságos megoldásokat kapjanak. Büszkék vagyunk arra, hogy sok ügyfelünkkel évek óta tartjuk a kapcsolatot, és számos projektet vittünk sikerre közösen."
|
||||
]
|
||||
},
|
||||
"mission": {
|
||||
"title": "Küldetésünk és értékeink",
|
||||
"subtitle": "Minden nap azért dolgozunk, hogy ügyfeleink digitális jelenléte biztonságos, stabil és hatékony legyen.",
|
||||
"values": [
|
||||
{
|
||||
"icon": "🛡️",
|
||||
"title": "Megbízhatóság",
|
||||
"description": "99.9% uptime és 24/7 monitoring biztosítja, hogy szolgáltatásaink mindig elérhetők legyenek."
|
||||
},
|
||||
{
|
||||
"icon": "👥",
|
||||
"title": "Személyes kapcsolat",
|
||||
"description": "Minden ügyfél számít számunkra. Személyre szabott megoldásokat kínálunk és mindig elérhetők vagyunk."
|
||||
},
|
||||
{
|
||||
"icon": "⚡",
|
||||
"title": "Műszaki kiválóság",
|
||||
"description": "Korszerű technológiák és bevált gyakorlatok alkalmazásával biztosítjuk a legmagasabb színvonalú szolgáltatást."
|
||||
}
|
||||
]
|
||||
},
|
||||
"team": {
|
||||
"title": "Szakértő csapat",
|
||||
"subtitle": "Tapasztalt IT szakemberek, akik szenvedélyesen dolgoznak az ügyfeleink sikeréért",
|
||||
"paragraphs": [
|
||||
"Csapatunk rendszeradminisztrátorokból, webfejlesztőkből és ügyfélszolgálati szakértőkből áll. Mindannyian több mint 10 éves tapasztalattal rendelkeznek a maguk területén, és folyamatosan követik a technológiai újdonságokat.",
|
||||
"Hiszünk abban, hogy a jó kommunikáció és a műszaki tudás együtt teremti meg a tökéletes ügyfélélményt. Ezért minden munkatársunk nemcsak technikai szakértő, hanem kiváló kommunikátor is."
|
||||
]
|
||||
},
|
||||
"cta": {
|
||||
"title": "Legyen Ön is elégedett ügyfelünk!",
|
||||
"subtitle": "Vegye fel velünk a kapcsolatot, és beszéljük meg, hogyan segíthetünk Önnek.",
|
||||
"button": "Kapcsolatfelvétel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Kapcsolat",
|
||||
"description": "Vegye fel velünk a kapcsolatot! Szívesen segítünk minden IT kérdésében."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Kapcsolat",
|
||||
"subtitle": "Vegye fel velünk a kapcsolatot! Szívesen segítünk minden IT kérdésében."
|
||||
},
|
||||
"form": {
|
||||
"title": "Küldjön üzenetet",
|
||||
"successMessage": "Köszönjük üzenetét! Hamarosan felvesszük Önnel a kapcsolatot.",
|
||||
"errorMessage": "Hiba történt az üzenet küldése során. Kérjük, próbálja újra vagy írjon közvetlenül a {email} címre.",
|
||||
"fields": {
|
||||
"name": {
|
||||
"label": "Név",
|
||||
"placeholder": "Az Ön neve",
|
||||
"error": "A név megadása kötelező"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email cím",
|
||||
"placeholder": "pelda@email.hu",
|
||||
"errorRequired": "Az email cím megadása kötelező",
|
||||
"errorInvalid": "Érvénytelen email cím formátum"
|
||||
},
|
||||
"subject": {
|
||||
"label": "Tárgy",
|
||||
"placeholder": "Miben segíthetünk?",
|
||||
"error": "A tárgy megadása kötelező"
|
||||
},
|
||||
"message": {
|
||||
"label": "Üzenet",
|
||||
"placeholder": "Írja le részletesen kérését vagy kérdését...",
|
||||
"errorRequired": "Az üzenet megadása kötelező",
|
||||
"errorMinLength": "Az üzenet legalább 10 karakter hosszú legyen"
|
||||
},
|
||||
"gdpr": {
|
||||
"label": "Elfogadom az <a href=\"/adatkezelesi-tajekoztato\" class=\"text-blue-600 hover:text-blue-700 underline\">adatkezelési tájékoztatót</a> és hozzájárulok személyes adataim kezeléséhez a kapcsolatfelvétel céljából.",
|
||||
"error": "Az adatkezelési tájékoztató elfogadása kötelező"
|
||||
}
|
||||
},
|
||||
"submitButton": "Üzenet küldése",
|
||||
"submittingButton": "Küldés..."
|
||||
},
|
||||
"info": {
|
||||
"title": "Elérhetőségek",
|
||||
"email": {
|
||||
"title": "Email",
|
||||
"responseTime": "24 órán belül válaszolunk"
|
||||
},
|
||||
"company": {
|
||||
"title": "Cég"
|
||||
},
|
||||
"webmail": {
|
||||
"title": "Webmail hozzáférés",
|
||||
"linkText": "Webmail belépés →",
|
||||
"subtitle": "Ügyfeleink számára"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "Gyakori kérdések",
|
||||
"items": [
|
||||
{
|
||||
"question": "Milyen gyorsan válaszolnak?",
|
||||
"answer": "Email üzenetekre 24 órán belül, sürgős esetekben telefonon is elérhetők vagyunk."
|
||||
},
|
||||
{
|
||||
"question": "Van ingyenes konzultáció?",
|
||||
"answer": "Igen! Az első konzultáció mindig ingyenes, hogy megismerjük az Ön igényeit."
|
||||
},
|
||||
{
|
||||
"question": "Milyen fizetési módokat fogadnak el?",
|
||||
"answer": "Banki átutalás, PayPal és kártyás fizetés is lehetséges."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cta": {
|
||||
"title": "Kapcsolatfelvétel az első lépés",
|
||||
"subtitle": "Mutassuk meg, hogyan segíthetünk Önnek megvalósítani céljait!",
|
||||
"button": "Kapcsolatfelvétel"
|
||||
},
|
||||
"serviceFeatures": {
|
||||
"title": "Szolgáltatás jellemzők:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Szolgáltatások",
|
||||
"description": "Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten. Ismerje meg részletes szolgáltatásainkat.",
|
||||
"ogDescription": "Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Szolgáltatásaink",
|
||||
"subtitle": "Teljes körű IT megoldások kisvállalkozások és magánszemélyek számára"
|
||||
},
|
||||
"details": {
|
||||
"title": "Részletes szolgáltatásleírás",
|
||||
"subtitle": "Minden szolgáltatásunk mögött évtizedes tapasztalat és modern technológia áll",
|
||||
"services": [
|
||||
{
|
||||
"icon": "🌐",
|
||||
"title": "Web Hosting részletesen",
|
||||
"description": "Weboldalak biztonságos és gyors üzemeltetése SSD tárolással, automatikus biztonsági mentéssel és 24/7 monitoringgal. Támogatjuk a PHP, Python, Node.js technológiákat és MySQL/PostgreSQL adatbázisokat.",
|
||||
"specs": {
|
||||
"title": "Technikai specifikációk:",
|
||||
"items": [
|
||||
"SSD tárhely 10GB-tól 500GB-ig",
|
||||
"Havi adatforgalom: korlátlan",
|
||||
"SSL tanúsítványok (Let's Encrypt vagy prémium)",
|
||||
"CDN integráció a gyorsabb betöltésért",
|
||||
"Automatikus napi biztonsági mentés",
|
||||
"cPanel vagy egyedi admin felület"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "✉️",
|
||||
"title": "Email szolgáltatás részletesen",
|
||||
"description": "Professzionális email fiókok saját domain névvel, spam szűréssel és vírusvédelemmel. Webmail felület és IMAP/POP3/SMTP támogatás minden népszerű email klienssel.",
|
||||
"specs": {
|
||||
"title": "Email funkciók:",
|
||||
"items": [
|
||||
"Korlátlan email fiókok létrehozása",
|
||||
"5GB-50GB tárhelyet fiókként",
|
||||
"Webmail hozzáférés (Roundcube/SOGo)",
|
||||
"Mobilalkalmazás szinkronizáció",
|
||||
"Spam és vírusszűrés",
|
||||
"Email továbbítás és automatikus válaszok",
|
||||
"Backup és archiválás"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "⚙️",
|
||||
"title": "DNS adminisztráció részletesen",
|
||||
"description": "Teljes DNS kezelés domain regisztrációval, átvitellel és professzionális beállításokkal. Gyors propagáció és megbízható névszerverek világszerte.",
|
||||
"specs": {
|
||||
"title": "DNS szolgáltatások:",
|
||||
"items": [
|
||||
"Domain regisztráció (.hu, .com, .eu, stb.)",
|
||||
"Domain átvitel más szolgáltatótól",
|
||||
"DNS rekord kezelés (A, CNAME, MX, TXT)",
|
||||
"Subdomain beállítások",
|
||||
"Redirect és forwarding szolgáltatások",
|
||||
"DNSSEC támogatás",
|
||||
"API hozzáférés fejlesztőknek"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"support": {
|
||||
"title": "Műszaki támogatás",
|
||||
"subtitle": "Minden szolgáltatásunkhoz teljes körű műszaki támogatást biztosítunk",
|
||||
"channels": [
|
||||
{
|
||||
"icon": "✉️",
|
||||
"title": "Email támogatás",
|
||||
"description": "24 órán belüli válasz"
|
||||
},
|
||||
{
|
||||
"icon": "📞",
|
||||
"title": "Telefonos segítség",
|
||||
"description": "Munkaidőben elérhető"
|
||||
},
|
||||
{
|
||||
"icon": "🚨",
|
||||
"title": "Sürgős esetek",
|
||||
"description": "Azonnali beavatkozás"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cta": {
|
||||
"title": "Kezdjük el a közös munkát!",
|
||||
"subtitle": "Vegye fel velünk a kapcsolatot ingyenes konzultációért és egyedi ajánlatért.",
|
||||
"primaryButton": "Kapcsolatfelvétel",
|
||||
"secondaryButton": "Webmail belépés"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Content Types - TypeScript definitions for JSON content files
|
||||
* These types ensure type safety when working with page content
|
||||
*/
|
||||
|
||||
// Common content types
|
||||
export interface CTAContent {
|
||||
text: string
|
||||
href: string
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
export interface IconTextItem {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface FAQItem {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export interface TechSpec {
|
||||
title: string
|
||||
items: string[]
|
||||
}
|
||||
|
||||
// Page-specific content types
|
||||
export interface HomePageContent {
|
||||
cta: {
|
||||
title: string
|
||||
subtitle: string
|
||||
button: string
|
||||
}
|
||||
serviceFeatures: {
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AboutPageContent {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
ogDescription: string
|
||||
}
|
||||
hero: {
|
||||
title: string
|
||||
subtitle: string
|
||||
}
|
||||
story: {
|
||||
title: string
|
||||
paragraphs: string[]
|
||||
}
|
||||
mission: {
|
||||
title: string
|
||||
subtitle: string
|
||||
values: IconTextItem[]
|
||||
}
|
||||
team: {
|
||||
title: string
|
||||
subtitle: string
|
||||
paragraphs: string[]
|
||||
}
|
||||
cta: {
|
||||
title: string
|
||||
subtitle: string
|
||||
button: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceDetailContent {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
specs: TechSpec
|
||||
}
|
||||
|
||||
export interface ServicesPageContent {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
ogDescription: string
|
||||
}
|
||||
hero: {
|
||||
title: string
|
||||
subtitle: string
|
||||
}
|
||||
details: {
|
||||
title: string
|
||||
subtitle: string
|
||||
services: ServiceDetailContent[]
|
||||
}
|
||||
support: {
|
||||
title: string
|
||||
subtitle: string
|
||||
channels: IconTextItem[]
|
||||
}
|
||||
cta: {
|
||||
title: string
|
||||
subtitle: string
|
||||
primaryButton: string
|
||||
secondaryButton: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ContactPageContent {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
hero: {
|
||||
title: string
|
||||
subtitle: string
|
||||
}
|
||||
form: {
|
||||
title: string
|
||||
successMessage: string
|
||||
errorMessage: string
|
||||
fields: {
|
||||
name: { label: string; placeholder: string; error: string }
|
||||
email: { label: string; placeholder: string; errorRequired: string; errorInvalid: string }
|
||||
subject: { label: string; placeholder: string; error: string }
|
||||
message: { label: string; placeholder: string; errorRequired: string; errorMinLength: string }
|
||||
gdpr: { label: string; error: string }
|
||||
}
|
||||
submitButton: string
|
||||
submittingButton: string
|
||||
}
|
||||
info: {
|
||||
title: string
|
||||
email: {
|
||||
title: string
|
||||
responseTime: string
|
||||
}
|
||||
company: {
|
||||
title: string
|
||||
}
|
||||
webmail: {
|
||||
title: string
|
||||
linkText: string
|
||||
subtitle: string
|
||||
}
|
||||
}
|
||||
faq: {
|
||||
title: string
|
||||
items: FAQItem[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommonContent {
|
||||
buttons: {
|
||||
contact: string
|
||||
learnMore: string
|
||||
webmail: string
|
||||
sendMessage: string
|
||||
}
|
||||
labels: {
|
||||
required: string
|
||||
features: string
|
||||
}
|
||||
validation: {
|
||||
required: string
|
||||
invalidEmail: string
|
||||
minLength: string
|
||||
}
|
||||
}
|
||||
|
||||
// Main content structure
|
||||
export interface SiteContent {
|
||||
common: CommonContent
|
||||
pages: {
|
||||
home: HomePageContent
|
||||
about: AboutPageContent
|
||||
services: ServicesPageContent
|
||||
contact: ContactPageContent
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,9 +7,9 @@
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
|
||||
Reference in New Issue
Block a user