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:
Do Siki
2026-01-24 02:30:27 +01:00
parent db464d1c48
commit d1213f9b3f
27 changed files with 3294 additions and 450 deletions
+429
View File
@@ -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 }
+316
View File
@@ -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]
+175
View File
@@ -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
}
+42 -3
View File
@@ -13,7 +13,12 @@ websitedev/
├── proto/ # Main Next.js application ├── proto/ # Main Next.js application
│ ├── src/ │ ├── src/
│ │ ├── app/ # Next.js App Router (pages and API routes) │ │ ├── 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) │ │ ├── lib/ # Utility libraries (MongoDB, Logger, Site Config)
│ │ ├── config/ # Static site configuration │ │ ├── config/ # Static site configuration
│ │ └── types/ # TypeScript type definitions │ │ └── types/ # TypeScript type definitions
@@ -28,6 +33,8 @@ websitedev/
- **Framework**: Next.js 14 with App Router and TypeScript - **Framework**: Next.js 14 with App Router and TypeScript
- **UI**: Tailwind CSS 4 with custom fonts (Geist Sans/Mono) - **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 - **Database**: MongoDB with Mongoose ODM
- **Logging**: Winston with Loki integration - **Logging**: Winston with Loki integration
- **Testing**: Jest with React Testing Library - **Testing**: Jest with React Testing Library
@@ -80,11 +87,43 @@ Required environment variables (check `.env` and `proto/.env.local`):
## Site Configuration ## Site Configuration
The site uses a centralized configuration system: 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/types/site.ts` - TypeScript interfaces for configuration
- `src/lib/site-config.ts` - Runtime configuration utilities - `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 ## Database Integration
+40 -1
View File
@@ -44,7 +44,9 @@ This project uses **separated sync systems** for optimal management:
## 🛠️ Tech Stack ## 🛠️ 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 - **Backend**: Next.js API Routes, MongoDB
- **Testing**: Jest, React Testing Library, Docker - **Testing**: Jest, React Testing Library, Docker
- **Deployment**: Dokploy, Docker - **Deployment**: Dokploy, Docker
@@ -59,6 +61,9 @@ This project uses **separated sync systems** for optimal management:
### ✅ Completed Features ### ✅ Completed Features
- [x] Responsive website with modern design - [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] Contact form with validation & spam protection
- [x] Rate limiting and security features - [x] Rate limiting and security features
- [x] Docker development environment - [x] Docker development environment
@@ -90,3 +95,37 @@ This project uses **separated sync systems** for optimal management:
- [Traceability Matrix](./TRACEABILITY-MATRIX.md) - [Traceability Matrix](./TRACEABILITY-MATRIX.md)
- [Docker Setup](./DOCKER.md) - [Docker Setup](./DOCKER.md)
- [Development Guide](./proto/README.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')
```
+5 -3
View File
@@ -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-33 | Kapcsolat űrlap + API stub | ✅ |
| ZEE-34 | /api/health endpoint | ✅ | | ZEE-34 | /api/health endpoint | ✅ |
| ZEE-35 | Dockerfile + Docker Compose fejlesztői környezet | ✅ | | 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) | ✅ | | - | Unit teszt infrastruktúra beállítása (Jest + React Testing Library) | ✅ |
| - | API endpoint tesztek írása (/api/health) | ✅ | | - | API endpoint tesztek írása (/api/health) | ✅ |
| - | Component tesztek írása (Header, Footer) | ✅ | | - | 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) ## 🔄 Folyamatban (Linear szerint)
| Linear Ticket | Feladat | Státusz | | Linear Ticket | Feladat | Státusz |
|---------------|---------|---------| |---------------|---------|---------|
| - | Jelenleg nincs aktív folyamatban lévő feladat | - | | - | ZEE-152 | - |
## 📋 Tervezett (Backlog) ## 📋 Tervezett (Backlog)
| Linear Ticket | Feladat | Státusz | | Linear Ticket | Feladat | Státusz |
|---------------|---------|---------| |---------------|---------|---------|
| ZEE-29 | Tailwind + alap layout | 📋 |
| ZEE-36 | CI (lint, unit) + Staging deploy trigger | 📋 | | ZEE-36 | CI (lint, unit) + Staging deploy trigger | 📋 |
| ZEE-37 | Playwright smoke E2E + Lighthouse CI | 📋 | | ZEE-37 | Playwright smoke E2E + Lighthouse CI | 📋 |
| ZEE-38 | Prod app + domain + HTTPS | 📋 | | 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 **Backlog ticketek:** ZEE-29, ZEE-36-46
**Duplikált ticketek eltávolítva:** ZEE-27 (duplikáció a ZEE-28-hoz képest) **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
+171
View File
@@ -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
+74
View File
@@ -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
View File
@@ -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 ```bash
# Install dependencies
npm install
# Development server (with Turbopack)
npm run dev npm run dev
# or
yarn dev # Production build
# or npm run build
pnpm dev
# or # Run tests
bun dev 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. ### CSS Variables
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - 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
View File
@@ -1,26 +1,556 @@
@import "tailwindcss"; @import "tailwindcss";
/* ========================================
mozdIT Bt. Design System
======================================== */
/* Design Tokens - Light Theme */
:root { :root {
--background: #ffffff; /* Brand Colors */
--foreground: #171717; --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 { /* Dark Mode */
--color-background: var(--background); [data-theme="dark"] {
--color-foreground: var(--foreground); --color-background: #0f172a;
--font-sans: var(--font-geist-sans); --color-background-secondary: #1e293b;
--font-mono: var(--font-geist-mono); --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) { @media (prefers-color-scheme: dark) {
:root { :root:not([data-theme="light"]) {
--background: #0a0a0a; --color-background: #0f172a;
--foreground: #ededed; --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 { body {
background: var(--background); background: var(--color-background);
color: var(--foreground); color: var(--color-foreground);
font-family: Arial, Helvetica, sans-serif; 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;
}
} }
+7 -4
View File
@@ -1,12 +1,15 @@
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { content } from '@/content'
import type { Metadata } from 'next' import type { Metadata } from 'next'
const { contact: pageContent } = content.pages
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Kapcsolat | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${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.', description: pageContent.meta.description,
openGraph: { openGraph: {
title: `Kapcsolat | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
description: 'Vegye fel velünk a kapcsolatot! Segítünk minden IT kérdésében.', description: pageContent.meta.description,
url: `${siteConfig.general.url}/kapcsolat`, url: `${siteConfig.general.url}/kapcsolat`,
}, },
} }
+42 -53
View File
@@ -1,8 +1,11 @@
'use client' 'use client'
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { content } from '@/content'
import { useState } from 'react' import { useState } from 'react'
const { contact: pageContent } = content.pages
export default function ContactPage() { export default function ContactPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -20,27 +23,27 @@ export default function ContactPage() {
const newErrors: Record<string, string> = {} const newErrors: Record<string, string> = {}
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = 'A név megadása kötelező' newErrors.name = pageContent.form.fields.name.error
} }
if (!formData.email.trim()) { 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)) { } 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()) { if (!formData.subject.trim()) {
newErrors.subject = 'A tárgy megadása kötelező' newErrors.subject = pageContent.form.fields.subject.error
} }
if (!formData.message.trim()) { 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) { } 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) { if (!formData.gdprConsent) {
newErrors.gdprConsent = 'Az adatkezelési tájékoztató elfogadása kötelező' newErrors.gdprConsent = pageContent.form.fields.gdpr.error
} }
setErrors(newErrors) setErrors(newErrors)
@@ -93,7 +96,6 @@ export default function ContactPage() {
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
})) }))
// Clear error when user starts typing
if (errors[name]) { if (errors[name]) {
setErrors(prev => ({ ...prev, [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"> <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"> <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"> <h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Kapcsolat {pageContent.hero.title}
</h1> </h1>
<p className="text-xl text-gray-600 leading-relaxed"> <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> </p>
</div> </div>
</section> </section>
@@ -119,13 +121,13 @@ export default function ContactPage() {
<div> <div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-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"> <h2 className="text-2xl font-bold text-gray-900 mb-6">
Küldjön üzenetet {pageContent.form.title}
</h2> </h2>
{submitStatus === 'success' && ( {submitStatus === 'success' && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-md"> <div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-800"> <p className="text-green-800">
Köszönjük üzenetét! Hamarosan felvesszük Önnel a kapcsolatot. {pageContent.form.successMessage}
</p> </p>
</div> </div>
)} )}
@@ -133,7 +135,7 @@ export default function ContactPage() {
{submitStatus === 'error' && ( {submitStatus === 'error' && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md"> <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800"> <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> </p>
</div> </div>
)} )}
@@ -141,7 +143,7 @@ export default function ContactPage() {
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Név * {pageContent.form.fields.name.label} *
</label> </label>
<input <input
type="text" 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 ${ 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' 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>} {errors.name && <p className="mt-1 text-sm text-red-600">{errors.name}</p>}
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email cím * {pageContent.form.fields.email.label} *
</label> </label>
<input <input
type="email" 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 ${ 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' 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>} {errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
</div> </div>
<div> <div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
Tárgy * {pageContent.form.fields.subject.label} *
</label> </label>
<input <input
type="text" 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 ${ 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' 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>} {errors.subject && <p className="mt-1 text-sm text-red-600">{errors.subject}</p>}
</div> </div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Üzenet * {pageContent.form.fields.message.label} *
</label> </label>
<textarea <textarea
id="message" 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 ${ 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' 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>} {errors.message && <p className="mt-1 text-sm text-red-600">{errors.message}</p>}
</div> </div>
@@ -220,9 +222,10 @@ export default function ContactPage() {
onChange={handleInputChange} onChange={handleInputChange}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" 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"> <span
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. * className="text-sm text-gray-700"
</span> dangerouslySetInnerHTML={{ __html: pageContent.form.fields.gdpr.label + ' *' }}
/>
</label> </label>
{errors.gdprConsent && <p className="mt-1 text-sm text-red-600">{errors.gdprConsent}</p>} {errors.gdprConsent && <p className="mt-1 text-sm text-red-600">{errors.gdprConsent}</p>}
</div> </div>
@@ -232,7 +235,7 @@ export default function ContactPage() {
disabled={isSubmitting} 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" 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> </button>
</form> </form>
</div> </div>
@@ -242,7 +245,7 @@ export default function ContactPage() {
<div className="space-y-8"> <div className="space-y-8">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-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"> <h2 className="text-2xl font-bold text-gray-900 mb-6">
Elérhetőségek {pageContent.info.title}
</h2> </h2>
<div className="space-y-6"> <div className="space-y-6">
@@ -251,7 +254,7 @@ export default function ContactPage() {
<span className="text-lg"></span> <span className="text-lg"></span>
</div> </div>
<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 <a
href={`mailto:${siteConfig.contact.email}`} href={`mailto:${siteConfig.contact.email}`}
className="text-blue-600 hover:text-blue-700" className="text-blue-600 hover:text-blue-700"
@@ -259,7 +262,7 @@ export default function ContactPage() {
{siteConfig.contact.email} {siteConfig.contact.email}
</a> </a>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
24 órán belül válaszolunk {pageContent.info.email.responseTime}
</p> </p>
</div> </div>
</div> </div>
@@ -269,7 +272,7 @@ export default function ContactPage() {
<span className="text-lg">🏢</span> <span className="text-lg">🏢</span>
</div> </div>
<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-gray-700">{siteConfig.general.name}</p>
<p className="text-sm text-gray-600">{siteConfig.contact.address}</p> <p className="text-sm text-gray-600">{siteConfig.contact.address}</p>
</div> </div>
@@ -280,17 +283,17 @@ export default function ContactPage() {
<span className="text-lg">🌐</span> <span className="text-lg">🌐</span>
</div> </div>
<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 <a
href={siteConfig.hero.cta.primary.href} href={siteConfig.hero.cta.primary.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700" className="text-blue-600 hover:text-blue-700"
> >
Webmail belépés {pageContent.info.webmail.linkText}
</a> </a>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
Ügyfeleink számára {pageContent.info.webmail.subtitle}
</p> </p>
</div> </div>
</div> </div>
@@ -300,30 +303,16 @@ export default function ContactPage() {
{/* FAQ */} {/* FAQ */}
<div className="bg-gray-50 rounded-xl p-8"> <div className="bg-gray-50 rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-6"> <h2 className="text-xl font-bold text-gray-900 mb-6">
Gyakori kérdések {pageContent.faq.title}
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> {pageContent.faq.items.map((item, index) => (
<h3 className="font-semibold text-gray-900 mb-2">Milyen gyorsan válaszolnak?</h3> <div key={index}>
<p className="text-gray-600 text-sm"> <h3 className="font-semibold text-gray-900 mb-2">{item.question}</h3>
Email üzenetekre 24 órán belül, sürgős esetekben telefonon is elérhetők vagyunk. <p className="text-gray-600 text-sm">{item.answer}</p>
</p> </div>
</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>
</div> </div>
</div> </div>
</div> </div>
+26 -6
View File
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Header from "../components/Header"; import Header from "../components/Header";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import { ThemeProvider } from "../components/ThemeProvider";
import { siteConfig } from "../config/site"; import { siteConfig } from "../config/site";
const geistSans = Geist({ const geistSans = Geist({
@@ -61,15 +62,34 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( 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 <body
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`} className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
style={{ background: 'var(--color-background)', color: 'var(--color-foreground)' }}
> >
<Header /> <ThemeProvider>
<main className="flex-1"> <Header />
{children} <main className="flex-1">
</main> {children}
<Footer /> </main>
<Footer />
</ThemeProvider>
</body> </body>
</html> </html>
); );
+198 -66
View File
@@ -1,25 +1,54 @@
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { content } from '@/content'
const { home: pageContent } = content.pages
export default function Home() { export default function Home() {
return ( return (
<div className="space-y-16"> <div className="space-y-0">
{/* Hero Section */} {/* Hero Section */}
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-20"> <section className="bg-gradient-hero py-20 lg:py-28 relative overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> {/* Background decoration */}
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight"> <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} {siteConfig.hero.title}
</h1> </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} {siteConfig.hero.description}
</p> </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 <a
href={siteConfig.hero.cta.primary.href} href={siteConfig.hero.cta.primary.href}
target={siteConfig.hero.cta.primary.external ? '_blank' : undefined} target={siteConfig.hero.cta.primary.external ? '_blank' : undefined}
rel={siteConfig.hero.cta.primary.external ? 'noopener noreferrer' : 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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10" />
</svg> </svg>
{siteConfig.hero.cta.primary.text} {siteConfig.hero.cta.primary.text}
@@ -27,7 +56,7 @@ export default function Home() {
{siteConfig.hero.cta.secondary && ( {siteConfig.hero.cta.secondary && (
<a <a
href={siteConfig.hero.cta.secondary.href} 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} {siteConfig.hero.cta.secondary.text}
</a> </a>
@@ -37,19 +66,44 @@ export default function Home() {
</section> </section>
{/* USP Section */} {/* USP Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 bg-gray-50"> <section
<div className="text-center"> className="py-20"
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-8"> style={{ background: 'var(--color-background-secondary)' }}
{siteConfig.about.title} >
</h2> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12"> <div className="text-center mb-12">
{siteConfig.about.usps.map((usp) => ( <h2
<div key={usp.id} className="text-center"> className="text-3xl md:text-4xl font-bold mb-4"
<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"> 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> <span className="text-2xl">{usp.icon}</span>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{usp.title}</h3> <h3
<p className="text-gray-600">{usp.description}</p> 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>
))} ))}
</div> </div>
@@ -57,64 +111,142 @@ export default function Home() {
</section> </section>
{/* Services Section */} {/* Services Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <section
<div className="text-center mb-12"> className="py-20"
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6"> style={{ background: 'var(--color-background)' }}
{siteConfig.services.title} >
</h2> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-lg text-gray-600 max-w-3xl mx-auto"> <div className="text-center mb-12">
{siteConfig.services.subtitle} <h2
</p> className="text-3xl md:text-4xl font-bold mb-4"
</div> style={{ color: 'var(--color-foreground)' }}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto"> >
{siteConfig.services.services.map((service) => ( {siteConfig.services.title}
<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"> </h2>
<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"> <p
<span className="text-xl">{service.icon}</span> 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> </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"> </div>
{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> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="bg-gray-900 text-white py-16"> <section
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> className="py-20 relative overflow-hidden"
<h2 className="text-3xl font-bold mb-4"> style={{ background: 'var(--color-foreground)' }}
Kapcsolatfelvétel az első lépés >
{/* 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> </h2>
<p className="text-xl text-gray-300 mb-8"> <p
Mutassuk meg, hogyan segíthetünk Önnek megvalósítani címeit! className="text-xl mb-8 max-w-2xl mx-auto"
style={{ color: 'var(--color-background)', opacity: 0.8 }}
>
{pageContent.cta.subtitle}
</p> </p>
<a <a
href="/kapcsolat" 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> </a>
</div> </div>
</section> </section>
</div> </div>
); )
} }
+141 -103
View File
@@ -1,153 +1,191 @@
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { content } from '@/content'
import type { Metadata } from 'next' import type { Metadata } from 'next'
const { about: pageContent } = content.pages
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Rólunk | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${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.', description: pageContent.meta.description,
openGraph: { openGraph: {
title: `Rólunk | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
description: 'Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit.', description: pageContent.meta.ogDescription,
url: `${siteConfig.general.url}/rolunk`, url: `${siteConfig.general.url}/rolunk`,
}, },
} }
export default function AboutPage() { export default function AboutPage() {
return ( return (
<div className="space-y-16 py-8"> <div className="space-y-0">
{/* Hero Section */} {/* Hero Section */}
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-16"> <section className="bg-gradient-hero py-16 lg:py-24 relative overflow-hidden">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="absolute inset-0 overflow-hidden pointer-events-none">
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"> <div
Rólunk 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> </h1>
<p className="text-xl text-gray-600 leading-relaxed"> <p
Több mint 10 éve biztosítunk megbízható IT infrastruktúrát és személyes ügyfélszolgálatot className="text-xl leading-relaxed animate-fade-in-up"
style={{ color: 'var(--color-foreground-muted)', animationDelay: '100ms' }}
>
{pageContent.hero.subtitle}
</p> </p>
</div> </div>
</section> </section>
{/* Story Section */} {/* Story Section */}
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <section
<div className="prose prose-lg mx-auto"> className="py-16"
<h2 className="text-3xl font-bold text-gray-900 mb-6">Történetünk</h2> style={{ background: 'var(--color-background)' }}
>
<div className="space-y-6 text-gray-700 leading-relaxed"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<p> <div className="prose prose-lg mx-auto">
A <strong>mozdIT Bt.</strong> 2010-ben alakult azzal a céllal, hogy kisvállalkozások és <h2
magánszemélyek számára nyújtson megbízható, személyes IT szolgáltatásokat. className="text-3xl font-bold mb-6"
Alapítóink több évtizedes tapasztalattal rendelkeznek a rendszeradminisztráció style={{ color: 'var(--color-foreground)' }}
és webfejlesztés területén. >
</p> {pageContent.story.title}
</h2>
<p> <div
Kezdetben néhány ügyfél weboldalának üzemeltetésével indultunk, ma pedig className="space-y-6 leading-relaxed"
több száz domain és email fiók működését biztosítjuk. Növekedésünk során style={{ color: 'var(--color-foreground-secondary)' }}
mindig szem előtt tartottuk az alapelveinket: <em>megbízhatóság, személyes >
kapcsolat és műszaki kiválóság</em>. {pageContent.story.paragraphs.map((paragraph, index) => (
</p> <p
key={index}
<p> dangerouslySetInnerHTML={{ __html: paragraph }}
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 </div>
számos projektet vittünk sikerre közösen.
</p>
</div> </div>
</div> </div>
</section> </section>
{/* Mission & Values */} {/* 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="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2
Küldetésünk és értékeink className="text-3xl font-bold mb-4"
style={{ color: 'var(--color-foreground)' }}
>
{pageContent.mission.title}
</h2> </h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto"> <p
Minden nap azért dolgozunk, hogy ügyfeleink digitális jelenléte biztonságos, className="text-lg max-w-3xl mx-auto"
stabil és hatékony legyen. style={{ color: 'var(--color-foreground-muted)' }}
>
{pageContent.mission.subtitle}
</p> </p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 stagger-children">
<div className="text-center"> {pageContent.mission.values.map((item, index) => (
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4"> <div
<span className="text-2xl">🛡</span> 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> </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>
</div> </div>
</section> </section>
{/* Team Section */} {/* Team Section */}
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <section
<div className="text-center mb-12"> className="py-16"
<h2 className="text-3xl font-bold text-gray-900 mb-4"> style={{ background: 'var(--color-background)' }}
Szakértő csapat >
</h2> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-lg text-gray-600"> <div className="text-center mb-12">
Tapasztalt IT szakemberek, akik szenvedélyesen dolgoznak az ügyfeleink sikeréért <h2
</p> className="text-3xl font-bold mb-4"
</div> style={{ color: 'var(--color-foreground)' }}
>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8"> {pageContent.team.title}
<div className="prose prose-lg mx-auto"> </h2>
<p className="text-gray-700 leading-relaxed"> <p
Csapatunk rendszeradminisztrátorokból, webfejlesztőkből és ügyfélszolgálati className="text-lg"
szakértőkből áll. Mindannyian több mint 10 éves tapasztalattal rendelkeznek style={{ color: 'var(--color-foreground-muted)' }}
a maguk területén, és folyamatosan követik a technológiai újdonságokat. >
</p> {pageContent.team.subtitle}
<p className="text-gray-700 leading-relaxed">
Hiszünk abban, hogy a 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> </p>
</div> </div>
<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> </div>
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="bg-gray-900 text-white py-16"> <section
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> className="py-16 relative overflow-hidden"
<h2 className="text-3xl font-bold mb-4"> style={{ background: 'var(--color-foreground)' }}
Legyen Ön is elégedett ügyfelünk! >
<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> </h2>
<p className="text-xl text-gray-300 mb-8"> <p
Vegye fel velünk a kapcsolatot, és beszéljük meg, hogyan segíthetünk Önnek. className="text-xl mb-8"
style={{ color: 'var(--color-background)', opacity: 0.8 }}
>
{pageContent.cta.subtitle}
</p> </p>
<a <a
href="/kapcsolat" 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> </a>
</div> </div>
</section> </section>
+42 -107
View File
@@ -1,12 +1,15 @@
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { content } from '@/content'
import type { Metadata } from 'next' import type { Metadata } from 'next'
const { services: pageContent } = content.pages
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Szolgáltatások | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
description: 'Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten. Ismerje meg részletes szolgáltatásainkat.', description: pageContent.meta.description,
openGraph: { openGraph: {
title: `Szolgáltatások | ${siteConfig.general.name}`, title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
description: 'Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten.', description: pageContent.meta.ogDescription,
url: `${siteConfig.general.url}/szolgaltatasok`, 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"> <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"> <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"> <h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Szolgáltatásaink {pageContent.hero.title}
</h1> </h1>
<p className="text-xl text-gray-600 leading-relaxed"> <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> </p>
</div> </div>
</section> </section>
@@ -39,7 +42,7 @@ export default function ServicesPage() {
<p className="text-gray-600 leading-relaxed mb-6">{service.description}</p> <p className="text-gray-600 leading-relaxed mb-6">{service.description}</p>
<div className="mb-6"> <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"> <ul className="space-y-2">
{service.features.map((feature, index) => ( {service.features.map((feature, index) => (
<li key={index} className="flex items-start"> <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="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-gray-900 mb-4">
Részletes szolgáltatásleírás {pageContent.details.title}
</h2> </h2>
<p className="text-lg text-gray-600"> <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> </p>
</div> </div>
<div className="space-y-12"> <div className="space-y-12">
{/* Web Hosting Details */} {pageContent.details.services.map((service) => (
<div className="bg-white rounded-xl p-8 shadow-sm"> <div key={service.title} className="bg-white rounded-xl p-8 shadow-sm">
<div className="flex items-start space-x-4"> <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"> <div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-xl">🌐</span> <span className="text-xl">{service.icon}</span>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="text-2xl font-bold text-gray-900 mb-4">Web Hosting részletesen</h3> <h3 className="text-2xl font-bold text-gray-900 mb-4">{service.title}</h3>
<div className="prose prose-lg text-gray-700"> <div className="prose prose-lg text-gray-700">
<p> <p>{service.description}</p>
Weboldalak biztonságos és gyors üzemeltetése SSD tárolással, automatikus biztonsági mentéssel <h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">{service.specs.title}</h4>
és 24/7 monitoringgal. Támogatjuk a PHP, Python, Node.js technológiákat és MySQL/PostgreSQL <ul className="space-y-1">
adatbázisokat. {service.specs.items.map((item, index) => (
</p> <li key={index}>{item}</li>
<h4 className="text-lg font-semibold text-gray-900 mt-6 mb-3">Technikai specifikációk:</h4> ))}
<ul className="space-y-1"> </ul>
<li>SSD tárhely 10GB-tól 500GB-ig</li> </div>
<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>
</div> </div>
</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>
</div> </div>
</section> </section>
@@ -168,24 +109,18 @@ export default function ServicesPage() {
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <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"> <div className="bg-blue-50 rounded-xl p-8 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 mb-4">
Műszaki támogatás {pageContent.support.title}
</h2> </h2>
<p className="text-lg text-gray-700 mb-6"> <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> </p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
<div> {pageContent.support.channels.map((channel) => (
<h3 className="font-semibold text-gray-900 mb-2">Email támogatás</h3> <div key={channel.title}>
<p className="text-gray-600">24 órán belüli válasz</p> <h3 className="font-semibold text-gray-900 mb-2">{channel.title}</h3>
</div> <p className="text-gray-600">{channel.description}</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>
</div> </div>
</div> </div>
</section> </section>
@@ -194,17 +129,17 @@ export default function ServicesPage() {
<section className="bg-gray-900 text-white py-16"> <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"> <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"> <h2 className="text-3xl font-bold mb-4">
Kezdjük el a közös munkát! {pageContent.cta.title}
</h2> </h2>
<p className="text-xl text-gray-300 mb-8"> <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> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<a <a
href="/kapcsolat" 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="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>
<a <a
href={siteConfig.hero.cta.primary.href} href={siteConfig.hero.cta.primary.href}
@@ -212,7 +147,7 @@ export default function ServicesPage() {
rel="noopener noreferrer" 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" 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> </a>
</div> </div>
</div> </div>
+124 -25
View File
@@ -1,36 +1,92 @@
'use client'
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
export default function Footer() { export default function Footer() {
return ( return (
<footer className="bg-gray-50 border-t border-gray-200"> <footer
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> className="border-t"
<div className="grid grid-cols-1 md:grid-cols-4 gap-8"> 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 */} {/* Company Info */}
<div className="md:col-span-2"> <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} {siteConfig.general.name}
</h3> <span
<p className="text-gray-600 mb-4 max-w-md"> 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} {siteConfig.general.description}
</p> </p>
<div className="flex space-x-4"> <div className="space-y-2 text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
<div className="text-sm text-gray-500"> <a
<p>Email: {siteConfig.contact.email}</p> href={`mailto:${siteConfig.contact.email}`}
<p>Cég: {siteConfig.general.name}</p> className="flex items-center gap-2 group transition-colors duration-200 hover:text-blue-600"
<p>Székhely: {siteConfig.contact.address}</p> 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> </div>
</div> </div>
{/* Navigation Links */} {/* Navigation Links */}
<div> <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ó Navigáció
</h3> </h3>
<ul className="space-y-2"> <ul className="space-y-3">
{siteConfig.navigation.footer.map((item) => ( {siteConfig.navigation.footer.map((item) => (
<li key={item.href}> <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} {item.label}
</a> </a>
</li> </li>
@@ -40,32 +96,59 @@ export default function Footer() {
{/* Services */} {/* Services */}
<div> <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 Szolgáltatások
</h3> </h3>
<ul className="space-y-2"> <ul className="space-y-3">
{siteConfig.services.services.map((service) => ( {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} {service.title}
</li> </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> </ul>
</div> </div>
</div> </div>
{/* Bottom section */} {/* Bottom section */}
<div className="border-t border-gray-200 pt-8 mt-8"> <div
<div className="flex flex-col sm:flex-row justify-between items-center"> className="border-t pt-8 mt-8"
<p className="text-gray-500 text-sm"> 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} {siteConfig.footer.copyright}
</p> </p>
<div className="flex space-x-4 mt-4 sm:mt-0"> <div className="flex items-center gap-6">
{siteConfig.footer.links.map((link) => ( {siteConfig.footer.links.map((link) => (
<a <a
key={link.href} key={link.href}
href={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} {link.label}
</a> </a>
@@ -73,7 +156,23 @@ export default function Footer() {
</div> </div>
</div> </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> </div>
</footer> </footer>
); )
} }
+158 -45
View File
@@ -1,93 +1,206 @@
'use client' 'use client'
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import { useState } from 'react' import { useState, useEffect } from 'react'
import { ThemeToggle } from './ThemeProvider'
export default function Header() { export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false) 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 = () => { const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen) 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 ( 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"> <nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
{/* Logo */} {/* Logo */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<a href="/" className="text-xl font-bold text-blue-600 hover:text-blue-700"> <a
{siteConfig.general.name} 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> </a>
</div> </div>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<div className="hidden md:block"> <div className="hidden md:flex items-center space-x-1">
<div className="flex items-center space-x-8"> {siteConfig.navigation.main.map((item, index) => (
{siteConfig.navigation.main.map((item) => ( <a
<a key={item.href}
key={item.href} href={item.href}
href={item.href} target={item.external ? '_blank' : undefined}
target={item.external ? '_blank' : undefined} rel={item.external ? 'noopener noreferrer' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined} className={`relative px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
className={item.label === 'Kapcsolat' item.label === 'Kapcsolat'
? "bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors" ? 'btn btn-primary text-white'
: "text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors" : '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} {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>
</div> </div>
{/* Mobile menu button */} {/* Mobile menu button */}
<div className="md:hidden"> <div className="md:hidden flex items-center gap-2">
<ThemeToggle />
<button <button
type="button" type="button"
onClick={toggleMenu} 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} aria-expanded={isMenuOpen}
> >
<span className="sr-only">{isMenuOpen ? 'Close main menu' : 'Open main menu'}</span> <span className="sr-only">{isMenuOpen ? 'Close main menu' : 'Open main menu'}</span>
{isMenuOpen ? ( <div className="relative w-6 h-6">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {/* Hamburger to X animation */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <span
</svg> className={`absolute h-0.5 w-6 rounded-full transform transition-all duration-300 ${
) : ( isMenuOpen ? 'rotate-45 top-3' : 'rotate-0 top-1'
<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" /> style={{ background: 'currentColor' }}
</svg> />
)} <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> </button>
</div> </div>
</div> </div>
{/* Mobile Navigation - Dynamic visibility */} {/* Mobile Navigation */}
<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 ${ <div
isMenuOpen className={`md:hidden overflow-hidden transition-all duration-300 ease-in-out ${
? 'opacity-100 visible' isMenuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
: 'opacity-0 invisible' }`}
}`}> >
<div className="px-2 pt-2 pb-3 space-y-1"> <div
{siteConfig.navigation.main.map((item) => ( className="py-3 space-y-1 border-t"
style={{ borderColor: 'var(--color-border)' }}
>
{siteConfig.navigation.main.map((item, index) => (
<a <a
key={item.href} key={item.href}
href={item.href} href={item.href}
target={item.external ? '_blank' : undefined} target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined} rel={item.external ? 'noopener noreferrer' : undefined}
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
className={item.label === 'Kapcsolat' className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
? "block px-3 py-2 rounded-md text-base font-medium bg-blue-600 text-white" item.label === 'Kapcsolat' ? 'btn btn-primary text-white mt-2' : ''
: "block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:text-blue-600 hover:bg-blue-50" }`}
} 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> </a>
))} ))}
</div> </div>
</div> </div>
</nav> </nav>
</header> </header>
); )
} }
+154
View File
@@ -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>
)
}
+17
View File
@@ -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"
}
}
+62
View File
@@ -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'
+53
View File
@@ -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"
}
}
+77
View File
@@ -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."
}
]
}
}
+10
View File
@@ -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:"
}
}
+94
View File
@@ -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"
}
}
+178
View File
@@ -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
View File
@@ -7,9 +7,9 @@
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,