Initial Commit
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# All
|
||||||
|
/.idea/
|
||||||
|
/rheinsw-mono-repo.iml
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
/backend/common/target/
|
||||||
|
/backend/discovery/target/
|
||||||
|
/backend/gateway/target/
|
||||||
|
/backend/server/target/
|
||||||
12
.run/npm-dev.run.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="npm-dev" type="js.build_tools.npm">
|
||||||
|
<package-json value="$PROJECT_DIR$/frontend/package.json" />
|
||||||
|
<command value="run" />
|
||||||
|
<scripts>
|
||||||
|
<script value="dev" />
|
||||||
|
</scripts>
|
||||||
|
<node-interpreter value="project" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
88
backend/common/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
|
<jackson-databind.version>2.18.3</jackson-databind.version>
|
||||||
|
<lombok.version>1.18.38</lombok.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>${maven.compiler.plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring stuff -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Others -->
|
||||||
|
<!-- https://mvnrepository.com/artifact/jakarta.persistence/jakarta.persistence-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.persistence</groupId>
|
||||||
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson-databind.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.rheinsw.shared.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 26.04.25
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@MappedSuperclass
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "createdDateTime", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdDateTime;
|
||||||
|
|
||||||
|
@Column(name = "modifiedDateTime")
|
||||||
|
private LocalDateTime modifiedDateTime;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdDateTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
modifiedDateTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.rheinsw.shared.mail;
|
||||||
|
|
||||||
|
import dev.rheinsw.shared.mail.dto.MailRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 22.04.25
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MailServiceClient {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MailServiceClient.class);
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
private static final String MAIL_ENDPOINT = "http://gateway/api/mail";
|
||||||
|
|
||||||
|
@Value("${INTERNAL_API_KEY}")
|
||||||
|
private String internalApiKey;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendMail(String email, String subject, String userMessage) {
|
||||||
|
MailRequest request = new MailRequest(email, subject, userMessage);
|
||||||
|
postEmail(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postEmail(MailRequest request) {
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("X-Internal-Auth", internalApiKey);
|
||||||
|
|
||||||
|
HttpEntity<MailRequest> entity = new HttpEntity<>(request, headers);
|
||||||
|
|
||||||
|
restTemplate.postForEntity(MAIL_ENDPOINT + "/send", entity, String.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send email to {}: {}", request.getTo(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.rheinsw.shared.mail.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 22.04.25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MailRequest {
|
||||||
|
private String to;
|
||||||
|
private String subject;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.rheinsw.shared.rest;
|
||||||
|
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 23.04.25
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@LoadBalanced
|
||||||
|
@Bean
|
||||||
|
public RestTemplate mailRestTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.rheinsw.shared.transport;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Bummsa / BoomerHD / Thatsaphorn Atchariyaphap
|
||||||
|
* @since 21.04.25
|
||||||
|
*/
|
||||||
|
public interface Dto extends Serializable {
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package dev.rheinsw.shared.entity;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class BaseEntityTest {
|
||||||
|
|
||||||
|
// Dummy entity for testing
|
||||||
|
static class DummyEntity extends BaseEntity {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onCreate_shouldSetCreatedDateTime() {
|
||||||
|
// Arrange
|
||||||
|
DummyEntity entity = new DummyEntity();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
entity.onCreate();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(entity.getCreatedDateTime(), "createdDateTime should be set");
|
||||||
|
assertNull(entity.getModifiedDateTime(), "modifiedDateTime should still be null after creation");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onUpdate_shouldSetModifiedDateTime() {
|
||||||
|
// Arrange
|
||||||
|
DummyEntity entity = new DummyEntity();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
entity.onUpdate();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(entity.getModifiedDateTime(), "modifiedDateTime should be set");
|
||||||
|
assertNull(entity.getCreatedDateTime(), "createdDateTime should still be null if onCreate() is not called");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onCreate_thenOnUpdate_shouldSetBothTimestamps() throws InterruptedException {
|
||||||
|
// Arrange
|
||||||
|
DummyEntity entity = new DummyEntity();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
entity.onCreate();
|
||||||
|
LocalDateTime created = entity.getCreatedDateTime();
|
||||||
|
Thread.sleep(10); // slight pause to differentiate timestamps
|
||||||
|
entity.onUpdate();
|
||||||
|
LocalDateTime modified = entity.getModifiedDateTime();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(created);
|
||||||
|
assertNotNull(modified);
|
||||||
|
assertTrue(modified.isAfter(created), "modifiedDateTime should be after createdDateTime");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.rheinsw.shared.mail;
|
||||||
|
|
||||||
|
import dev.rheinsw.shared.mail.dto.MailRequest;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class MailServiceClientTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private MailServiceClient mailServiceClient;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<HttpEntity<MailRequest>> httpEntityCaptor;
|
||||||
|
|
||||||
|
private AutoCloseable closeable;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
closeable = MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendMail_shouldSendCorrectRequest() {
|
||||||
|
// Arrange
|
||||||
|
String email = "user@example.com";
|
||||||
|
String subject = "Test Subject";
|
||||||
|
String message = "This is a test message.";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mailServiceClient.sendMail(email, subject, message);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(restTemplate).postForEntity(
|
||||||
|
eq("http://gateway/api/mail/send"),
|
||||||
|
httpEntityCaptor.capture(),
|
||||||
|
eq(String.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
MailRequest captured = httpEntityCaptor.getValue().getBody(); // extract the MailRequest
|
||||||
|
assert captured != null;
|
||||||
|
assert captured.getTo().equals(email);
|
||||||
|
assert captured.getSubject().equals(subject);
|
||||||
|
assert captured.getMessage().equals(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendMail_shouldHandleExceptionDuringPost() {
|
||||||
|
// Arrange
|
||||||
|
String email = "user@example.com";
|
||||||
|
String subject = "Test Subject";
|
||||||
|
String message = "This is a test message.";
|
||||||
|
|
||||||
|
doThrow(new RuntimeException("Simulated error")).when(restTemplate).postForEntity(
|
||||||
|
anyString(),
|
||||||
|
any(),
|
||||||
|
eq(String.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertDoesNotThrow(() -> mailServiceClient.sendMail(email, subject, message),
|
||||||
|
"sendMail should handle exception internally and not throw it");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
closeable.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.rheinsw.shared.rest;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RestTemplateConfigTest {
|
||||||
|
|
||||||
|
private final RestTemplateConfig restTemplateConfig = new RestTemplateConfig();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mailRestTemplate_shouldReturnNonNullRestTemplate() {
|
||||||
|
// Act
|
||||||
|
RestTemplate restTemplate = restTemplateConfig.mailRestTemplate();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(restTemplate, "RestTemplate should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mailRestTemplate_shouldCreateNewInstanceEachTime() {
|
||||||
|
// Act
|
||||||
|
RestTemplate restTemplate1 = restTemplateConfig.mailRestTemplate();
|
||||||
|
RestTemplate restTemplate2 = restTemplateConfig.mailRestTemplate();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotSame(restTemplate1, restTemplate2, "Each call should create a new RestTemplate instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/discovery/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>discovery</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.rheinsw.discovery;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Bummsa / BoomerHD / Thatsaphorn Atchariyaphap
|
||||||
|
* @since 21.04.25
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableEurekaServer
|
||||||
|
public class DiscoveryServerApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DiscoveryServerApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
backend/discovery/src/main/resources/application.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
server:
|
||||||
|
port: 8761
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: discovery-server
|
||||||
|
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
register-with-eureka: false
|
||||||
|
fetch-registry: false
|
||||||
|
server:
|
||||||
|
wait-time-in-ms-when-sync-empty: 0
|
||||||
30
backend/gateway/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>dev.rheinsw.backend</groupId>
|
||||||
|
<artifactId>gateway</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>common</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
11
backend/gateway/src/main/java/dev/rheinsw/gateway/Main.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.rheinsw.gateway;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 04.05.25
|
||||||
|
*/
|
||||||
|
public class Main {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("Hello, World!");
|
||||||
|
}
|
||||||
|
}
|
||||||
82
backend/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>common</module>
|
||||||
|
<module>discovery</module>
|
||||||
|
<module>gateway</module>
|
||||||
|
<module>server</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<maven.compiler.plugin.version>3.14.0</maven.compiler.plugin.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
|
<!-- Spring-related stuff -->
|
||||||
|
<spring-boot.version>3.4.4</spring-boot.version>
|
||||||
|
<spring-cloud.version>2024.0.1</spring-cloud.version>
|
||||||
|
|
||||||
|
<!-- all the others -->
|
||||||
|
<lombok.version>1.18.38</lombok.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot BOM -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Cloud BOM -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-dependencies</artifactId>
|
||||||
|
<version>${spring-cloud.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Other BOMs or shared deps -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.17.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.3</version>
|
||||||
|
<configuration>
|
||||||
|
<argLine>-Djdk.attach.allowAttachSelf=true</argLine>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
30
backend/server/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>dev.rheinsw.backend</groupId>
|
||||||
|
<artifactId>server</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.rheinsw</groupId>
|
||||||
|
<artifactId>common</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
11
backend/server/src/main/java/dev/rheinsw/server/Main.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.rheinsw.server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Thatsaphorn Atchariyaphap
|
||||||
|
* @since 04.05.25
|
||||||
|
*/public class Main {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("Hello, World!");
|
||||||
|
}
|
||||||
|
}
|
||||||
41
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
35
frontend/.gitlab-ci.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
stages:
|
||||||
|
- frontend_build
|
||||||
|
- dockerize
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: ${CI_COMMIT_REF_SLUG}
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
- .next/cache/
|
||||||
|
|
||||||
|
variables:
|
||||||
|
NEXT_PUBLIC_ENV: "production"
|
||||||
|
OUTPUT_DIR: ".next"
|
||||||
|
PROJECT_NAME: $CI_PROJECT_NAME
|
||||||
|
DOCKER_IMAGE: $CI_REGISTRY_IMAGE
|
||||||
|
|
||||||
|
frontend_build_job:
|
||||||
|
stage: frontend_build
|
||||||
|
image: node:22@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95
|
||||||
|
script:
|
||||||
|
- cd frontend
|
||||||
|
- npm install
|
||||||
|
- npx next build
|
||||||
|
- npm run lint
|
||||||
|
|
||||||
|
dockerize_frontend:
|
||||||
|
stage: dockerize
|
||||||
|
extends: .docker_build_template
|
||||||
|
variables:
|
||||||
|
DOCKER_CONTEXT: frontend
|
||||||
|
DOCKERFILE: Dockerfile
|
||||||
|
DOCKER_IMAGE: $DOCKER_IMAGE
|
||||||
|
DOCKER_BUILD_ARGS: ""
|
||||||
|
needs:
|
||||||
|
- job: frontend_build_job
|
||||||
115
frontend/.gitlab-ci_old.yml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
image: node:22@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
|
- if: $CI_COMMIT_BRANCH == "production"
|
||||||
|
- if: $CI_COMMIT_BRANCH == "dev"
|
||||||
|
- if: $CI_COMMIT_TAG =~ /^v[\d]{1,4}\.[\d]{1,2}\.[\d]{1,2}$/
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- docker
|
||||||
|
- deploy
|
||||||
|
- sync
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: ${CI_COMMIT_REF_SLUG}
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
- .next/cache/
|
||||||
|
|
||||||
|
variables:
|
||||||
|
NEXT_PUBLIC_ENV: "production"
|
||||||
|
OUTPUT_DIR: ".next"
|
||||||
|
PROJECT_NAME: $CI_PROJECT_NAME
|
||||||
|
DOCKER_IMAGE: "registry.boomlab.party/rheinsw/$CI_PROJECT_NAME"
|
||||||
|
|
||||||
|
.deploy_production_rule: &deploy_production_rule
|
||||||
|
- if: $CI_COMMIT_BRANCH == "production"
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
# Reusable SSH key setup block
|
||||||
|
.install_deploy_key: &install_deploy_key
|
||||||
|
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$DEPLOY_KEY_BASE64" | base64 -d > ~/.ssh/deploy_key
|
||||||
|
- eval "$(ssh-agent -s)"
|
||||||
|
- chmod 600 ~/.ssh/deploy_key
|
||||||
|
- ssh-add ~/.ssh/deploy_key
|
||||||
|
- ssh-keyscan -p 22 -H '192.168.41.101' >> ~/.ssh/known_hosts || true
|
||||||
|
|
||||||
|
.deploy_script: &deploy_script
|
||||||
|
- |
|
||||||
|
echo "Deploying $DOCKER_IMAGE:$TAG to $CONTAINER_NAME on port $PORT..."
|
||||||
|
|
||||||
|
ssh gitlab@192.168.41.101 -p 22 "
|
||||||
|
echo \"$CI_REGISTRY_PASSWORD\" | docker login $CI_REGISTRY -u \"$CI_REGISTRY_USER\" --password-stdin &&
|
||||||
|
docker pull $DOCKER_IMAGE:$TAG &&
|
||||||
|
docker stop $CONTAINER_NAME || true &&
|
||||||
|
docker rm $CONTAINER_NAME || true &&
|
||||||
|
docker run -d --name $CONTAINER_NAME -p $PORT:3000 $DOCKER_IMAGE:$TAG
|
||||||
|
"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- npm install
|
||||||
|
- npx next build
|
||||||
|
- npm run lint
|
||||||
|
|
||||||
|
dockerize:
|
||||||
|
stage: docker
|
||||||
|
image: docker:20.10@sha256:2967f0819c84dd589ed0a023b9d25dcfe7a3c123d5bf784ffbb77edf55335f0c
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
RAW_TAG="$CI_COMMIT_REF_NAME"
|
||||||
|
TAG="${RAW_TAG//\//_}" # replaces "/" with "_"
|
||||||
|
echo "Sanitized tag: $TAG"
|
||||||
|
docker build -t $DOCKER_IMAGE:$TAG -f Dockerfile .
|
||||||
|
|
||||||
|
if [[ "$RAW_TAG" == "dev" || "$RAW_TAG" == "production" ]]; then
|
||||||
|
echo "Pushing Docker image $DOCKER_IMAGE:$TAG"
|
||||||
|
docker push $DOCKER_IMAGE:$TAG
|
||||||
|
else
|
||||||
|
echo "Skipping Docker push for non-dev or production branch: $RAW_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
deploy_production:
|
||||||
|
stage: deploy
|
||||||
|
rules: *deploy_production_rule
|
||||||
|
before_script: *install_deploy_key
|
||||||
|
script:
|
||||||
|
- TAG="production"
|
||||||
|
- PORT="4100"
|
||||||
|
- CONTAINER_NAME="$CI_PROJECT_NAME-production"
|
||||||
|
- *deploy_script
|
||||||
|
|
||||||
|
deploy_dev:
|
||||||
|
stage: deploy
|
||||||
|
before_script: *install_deploy_key
|
||||||
|
script:
|
||||||
|
- TAG="dev"
|
||||||
|
- PORT="5100"
|
||||||
|
- CONTAINER_NAME="$CI_PROJECT_NAME-dev"
|
||||||
|
- *deploy_script
|
||||||
|
only:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
sync_branches:
|
||||||
|
stage: sync
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
before_script:
|
||||||
|
- git config --global user.email "gitlab-ci@rheinsw.com"
|
||||||
|
- git config --global user.name "GitLab CI"
|
||||||
|
script:
|
||||||
|
- git remote set-url origin "https://oauth2:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
||||||
|
- git checkout dev
|
||||||
|
- git pull origin dev
|
||||||
|
- git merge --no-ff origin/production
|
||||||
|
- git push origin dev
|
||||||
31
frontend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Use lightweight Node.js 20 base image
|
||||||
|
FROM node:20-alpine@sha256:9bef0ef1e268f60627da9ba7d7605e8831d5b56ad07487d24d1aa386336d1944 as builder
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files separately for better Docker caching
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy entire project
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the Next.js app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Use a minimal base image for running the app
|
||||||
|
FROM node:20-alpine@sha256:9bef0ef1e268f60627da9ba7d7605e8831d5b56ad07487d24d1aa386336d1944
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built files from the builder stage
|
||||||
|
COPY --from=builder /app ./
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start Next.js in production mode
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
36
frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
BIN
frontend/app/(root)/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
37
frontend/app/(root)/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {Metadata} from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import Nav from "@/components/Navbar/Nav";
|
||||||
|
import Footer from "@/components/Footer/Footer";
|
||||||
|
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||||
|
import React from "react";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Rhein Software",
|
||||||
|
description: "Rhein Software Development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||||
|
const bgColor = themeColors[theme].primaryBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de" data-theme={theme}>
|
||||||
|
<head/>
|
||||||
|
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Nav/>
|
||||||
|
{children}
|
||||||
|
<Footer/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
frontend/app/(root)/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Home from "@/components/Home/Home";
|
||||||
|
|
||||||
|
const HomePage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Home />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
37
frontend/app/about/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {Metadata} from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import Nav from "@/components/Navbar/Nav";
|
||||||
|
import Footer from "@/components/Footer/Footer";
|
||||||
|
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||||
|
import React from "react";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Über Uns | Rhein Software",
|
||||||
|
description: "Rhein Software Development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||||
|
const bgColor = themeColors[theme].primaryBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de" data-theme={theme}>
|
||||||
|
<head/>
|
||||||
|
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Nav/>
|
||||||
|
{children}
|
||||||
|
<Footer/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
frontend/app/about/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AboutContent from "@/components/About/AboutContent";
|
||||||
|
|
||||||
|
const AboutPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AboutContent/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutPage;
|
||||||
54
frontend/app/api/contact/route.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {NextRequest, NextResponse} from 'next/server';
|
||||||
|
|
||||||
|
const HCAPTCHA_SECRET = process.env.HCAPTCHA_SECRET ?? '';
|
||||||
|
const SHARED_API_KEY = process.env.SHARED_API_KEY ?? '';
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const origin = req.headers.get("origin") || "http://localhost:3000";
|
||||||
|
const captchaToken = body.captcha;
|
||||||
|
|
||||||
|
if (!captchaToken) {
|
||||||
|
return NextResponse.json({success: false, error: 'Captcha is required'}, {status: 400});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Verify hCaptcha token with their API
|
||||||
|
const verifyResponse = await fetch('https://api.hcaptcha.com/siteverify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
secret: HCAPTCHA_SECRET,
|
||||||
|
response: captchaToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const captchaResult = await verifyResponse.json();
|
||||||
|
|
||||||
|
if (!captchaResult.success) {
|
||||||
|
return NextResponse.json({success: false, error: 'Captcha verification failed'}, {status: 403});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Forward valid contact request to Spring Boot backend
|
||||||
|
const backendRes = await fetch('http://localhost:8080/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Origin": origin,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Frontend-Key': SHARED_API_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const backendText = await backendRes.text();
|
||||||
|
|
||||||
|
if (!backendRes.ok) {
|
||||||
|
return NextResponse.json({success: false, error: backendText}, {status: backendRes.status});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({success: true, message: backendText});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[ContactAPI] error:', err);
|
||||||
|
return NextResponse.json({success: false, error: err.message}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/app/contact/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {Metadata} from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import Nav from "@/components/Navbar/Nav";
|
||||||
|
import Footer from "@/components/Footer/Footer";
|
||||||
|
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||||
|
import React from "react";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Kontakt | Rhein Software",
|
||||||
|
description: "Rhein Software Development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||||
|
const bgColor = themeColors[theme].primaryBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de" data-theme={theme}>
|
||||||
|
<head/>
|
||||||
|
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Nav/>
|
||||||
|
{children}
|
||||||
|
<Footer/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
frontend/app/contact/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Contact from "@/components/Contact/Contact";
|
||||||
|
|
||||||
|
const ContactPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Contact/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
27
frontend/app/globals.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.nav__link {
|
||||||
|
@apply relative text-base font-medium w-fit block after:block after:content-[''] after:absolute after:h-[3px] after:bg-pink-600 after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global theme transition */
|
||||||
|
.transition-theme {
|
||||||
|
transition: background-color 0.7s ease, color 0.7s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
12
frontend/app/legal/imprint/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImprintComp from "@/components/Legal/Imprint/ImprintComp";
|
||||||
|
|
||||||
|
const ImprintPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ImprintComp />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImprintPage;
|
||||||
38
frontend/app/legal/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type {Metadata} from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import Nav from "@/components/Navbar/Nav";
|
||||||
|
import Footer from "@/components/Footer/Footer";
|
||||||
|
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||||
|
import React from "react";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Rechtliches | Rhein Software",
|
||||||
|
description: "Rhein Software Development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||||
|
const bgColor = themeColors[theme].primaryBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de" data-theme={theme}>
|
||||||
|
<head/>
|
||||||
|
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Nav/>
|
||||||
|
{children}
|
||||||
|
<Footer/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
10
frontend/app/legal/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LegalPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegalPage;
|
||||||
12
frontend/app/legal/privacy/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PrivacyComp from "@/components/Legal/Privacy/PrivacyComp";
|
||||||
|
|
||||||
|
const PrivacyPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PrivacyComp />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivacyPage;
|
||||||
12
frontend/app/legal/revocation/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import RevocationComp from "@/components/Legal/RevocationComp";
|
||||||
|
|
||||||
|
const RevocationPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<RevocationComp />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RevocationPage;
|
||||||
12
frontend/app/legal/terms-of-use/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TermsOfUseComp from "@/components/Legal/TermsOfUseComp";
|
||||||
|
|
||||||
|
const TermsOfUsePage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TermsOfUseComp/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TermsOfUsePage;
|
||||||
37
frontend/app/services/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {Metadata} from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import Nav from "@/components/Navbar/Nav";
|
||||||
|
import Footer from "@/components/Footer/Footer";
|
||||||
|
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||||
|
import React from "react";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Leistungen | Rhein Software",
|
||||||
|
description: "Rhein Software Development",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||||
|
const bgColor = themeColors[theme].primaryBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de" data-theme={theme}>
|
||||||
|
<head/>
|
||||||
|
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Nav/>
|
||||||
|
{children}
|
||||||
|
<Footer/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
frontend/app/services/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Services from "@/components/Services/Services";
|
||||||
|
|
||||||
|
const ContactPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Services/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
47
frontend/components/About/AboutContent.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import Section from "@/components/Section";
|
||||||
|
import AboutHero from "@/components/About/Section/AboutHero";
|
||||||
|
import AboutTimeline from "@/components/About/Section/AboutTimeline";
|
||||||
|
import TeamSection from "@/components/About/Section/TeamSection";
|
||||||
|
import AboutIntro from "@/components/About/Section/AboutIntro";
|
||||||
|
import AboutProcess from "@/components/About/Section/AboutProcess";
|
||||||
|
|
||||||
|
const AboutContent = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.7, ease: "easeOut"}}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<AboutHero/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||||
|
<AboutIntro/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<AboutProcess/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||||
|
<AboutTimeline/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<TeamSection/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutContent;
|
||||||
19
frontend/components/About/Section/AboutHero.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import SmallHero from "@/components/Helper/SmallHero";
|
||||||
|
|
||||||
|
const AboutHero = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<SmallHero
|
||||||
|
title="Über uns"
|
||||||
|
subtitle="Digitaler Partner für individuelle Softwarelösungen."
|
||||||
|
backgroundImage="/images/contact.png"
|
||||||
|
blurBackground
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutHero;
|
||||||
68
frontend/components/About/Section/AboutIntro.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
import {useThemeColors} from '@/utils/useThemeColors';
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative w-full py-24 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* Text */}
|
||||||
|
<div className="p-0 max-w-4xl">
|
||||||
|
<motion.p
|
||||||
|
className="text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir sind Rhein-Software – Ihr Partner für digitale Produkte und individuelle
|
||||||
|
Softwarelösungen.
|
||||||
|
Wir entwickeln skalierbare, wartbare Anwendungen mit klarem Fokus: Technik, die begeistert –
|
||||||
|
von der Architektur bis zum Go-Live.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="mt-6 text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.3}}
|
||||||
|
>
|
||||||
|
Ob Start-up oder etabliertes Unternehmen: Wir begleiten Sie mit einem flexiblen Netzwerk,
|
||||||
|
klarer Kommunikation und hohem Qualitätsanspruch – agil, lösungsorientiert und nah am
|
||||||
|
Projekt.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<motion.div
|
||||||
|
className="mt-10 flex justify-end"
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.5}}
|
||||||
|
>
|
||||||
|
{/*<Link href="/about">*/}
|
||||||
|
{/* <button*/}
|
||||||
|
{/* className="flex items-center gap-2 bg-blue-700 hover:bg-blue-900 text-white font-semibold px-5 py-2 rounded-full shadow-lg transition-all"*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* Mehr über uns <FiArrowRight size={18}/>*/}
|
||||||
|
{/* </button>*/}
|
||||||
|
{/*</Link>*/}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
158
frontend/components/About/Section/AboutProcess.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const processSteps = [
|
||||||
|
{
|
||||||
|
title: "Beratung",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
In der <strong>Beratungsphase</strong> analysieren wir gemeinsam Ihre Anforderungen und
|
||||||
|
Geschäftsziele. Dabei identifizieren wir Herausforderungen und definieren die Zielsetzung
|
||||||
|
für Ihr Projekt.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Planung",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Wir erarbeiten ein <strong>technisches Konzept</strong> mit klarer Struktur, Meilensteinen und
|
||||||
|
Ressourcenplanung. Eine solide Architektur bildet die Grundlage für ein skalierbares und wartbares
|
||||||
|
System.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Entwicklung",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
In iterativen Zyklen setzen wir das Projekt um. Regelmäßige <strong>Feedbackschleifen</strong>
|
||||||
|
sorgen dafür, dass das Ergebnis Ihren Erwartungen entspricht und flexibel angepasst werden kann.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Durch umfangreiche <strong>Tests und Optimierungen</strong> stellen wir sicher, dass Ihre
|
||||||
|
Anwendung robust, performant und benutzerfreundlich ist – noch vor dem Go-Live.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Go-Live",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Wir begleiten Sie beim <strong>produktiven Einsatz</strong> Ihrer Anwendung und unterstützen Sie
|
||||||
|
auch nach dem Go-Live mit Support und Weiterentwicklungsmöglichkeiten.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AboutProcess: React.FC = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full px-6 sm:px-12 py-20 max-w-6xl mx-auto">
|
||||||
|
<h2
|
||||||
|
className="text-2xl sm:text-3xl font-bold text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
Unser Prozess
|
||||||
|
</h2>
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Mobile View: Tab buttons */}
|
||||||
|
<div className="block md:hidden mb-6">
|
||||||
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
|
{processSteps.map((step, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setActiveIndex(idx)}
|
||||||
|
className={`w-full px-4 py-2 text-sm border rounded-full transition-colors ${
|
||||||
|
activeIndex === idx
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop View: 2-column layout */}
|
||||||
|
<div className="hidden md:grid grid-cols-3 gap-8">
|
||||||
|
{/* Left: Step List */}
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
{processSteps.map((step, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setActiveIndex(idx)}
|
||||||
|
className={`text-left px-4 py-3 border rounded-lg transition-colors ${
|
||||||
|
activeIndex === idx
|
||||||
|
? 'border-blue-600 bg-blue-50 dark:bg-gray-800'
|
||||||
|
: 'border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<span className="font-semibold">{idx + 1}. {step.title}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Step Content */}
|
||||||
|
<div className="md:col-span-2 p-6 border border-gray-300 dark:border-gray-700 rounded-lg"
|
||||||
|
style={{backgroundColor: colors.primaryBg}}>
|
||||||
|
<motion.div
|
||||||
|
key={activeIndex}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.4}}
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-bold mb-4" style={{color: colors.primaryText}}>
|
||||||
|
{processSteps[activeIndex].title}
|
||||||
|
</h3>
|
||||||
|
<div className="text-base space-y-1" style={{color: colors.secondaryText}}>
|
||||||
|
{processSteps[activeIndex].description}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile View: Content Below Tabs */}
|
||||||
|
<div className="block md:hidden">
|
||||||
|
<div className="p-6 border border-gray-300 dark:border-gray-700 rounded-lg"
|
||||||
|
style={{backgroundColor: colors.primaryBg}}>
|
||||||
|
<motion.div
|
||||||
|
key={activeIndex + '-mobile'}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.4}}
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-bold mb-4" style={{color: colors.primaryText}}>
|
||||||
|
{activeIndex + 1}. {processSteps[activeIndex].title}
|
||||||
|
</h3>
|
||||||
|
<div className="text-base space-y-1" style={{color: colors.secondaryText}}>
|
||||||
|
{processSteps[activeIndex].description}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutProcess;
|
||||||
94
frontend/components/About/Section/AboutTimeline.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const timeline = [
|
||||||
|
{
|
||||||
|
date: "Oktober 2024",
|
||||||
|
title: "Projektgründung",
|
||||||
|
description: "Entwicklung der Idee und erste Umsetzungsschritte – inspiriert durch Technik und Nachhaltigkeit.",
|
||||||
|
current: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Mai 2025",
|
||||||
|
title: "Go-Live",
|
||||||
|
description: "Offizieller Start mit Kundenprojekten und einem umfassenden Full-Service-Angebot.",
|
||||||
|
current: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AboutTimeline3 = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full px-6 sm:px-12 py-8 max-w-5xl mx-auto">
|
||||||
|
<h2
|
||||||
|
className="text-2xl sm:text-3xl font-bold mt-10"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
Von der Idee bis heute
|
||||||
|
</h2>
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative border-l-2 border-gray-300 dark:border-gray-700 ml-6">
|
||||||
|
{timeline.map((item, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: idx * 0.2}}
|
||||||
|
className="relative mb-10 pl-12"
|
||||||
|
>
|
||||||
|
{/* Timeline dot */}
|
||||||
|
<div className="absolute left-[-22px] top-0 z-10">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full border-2 flex items-center justify-center bg-white dark:bg-gray-900 ${
|
||||||
|
item.current
|
||||||
|
? "border-blue-600"
|
||||||
|
: "border-gray-400 dark:border-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.current && (
|
||||||
|
<motion.span
|
||||||
|
className="absolute w-10 h-10 rounded-full bg-blue-600 opacity-40"
|
||||||
|
animate={{scale: [1, 1.6, 1], opacity: [0.4, 0, 0.4]}}
|
||||||
|
transition={{repeat: Infinity, duration: 1.6}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline card */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{scale: 1.02, translateX: 4}}
|
||||||
|
transition={{type: "spring", stiffness: 260, damping: 20}}
|
||||||
|
className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-5 border border-gray-200 dark:border-gray-700 cursor-default"
|
||||||
|
>
|
||||||
|
<div className="text-sm text-blue-600 font-semibold mb-1">{item.date}</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-bold mb-2"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{color: colors.secondaryText}}>
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutTimeline3;
|
||||||
97
frontend/components/About/Section/TeamSection.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const team = [
|
||||||
|
{
|
||||||
|
name: "Thatsaphorn",
|
||||||
|
role: "Gründer & Entwickler",
|
||||||
|
picture: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Anonym",
|
||||||
|
role: "Vertrieb",
|
||||||
|
picture: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fallbackImage = "/images/team/default-avatar.jpg";
|
||||||
|
|
||||||
|
const TeamSection = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full px-6 sm:px-12 py-16 max-w-6xl mx-auto">
|
||||||
|
<motion.h2
|
||||||
|
className="text-2xl sm:text-3xl font-bold text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
Das Team
|
||||||
|
</motion.h2>
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div
|
||||||
|
className={`grid gap-8
|
||||||
|
grid-cols-1
|
||||||
|
sm:grid-cols-${Math.min(team.length, 2)}
|
||||||
|
md:grid-cols-${Math.min(team.length, 3)}
|
||||||
|
lg:grid-cols-${Math.min(team.length, 4)}`}
|
||||||
|
>
|
||||||
|
{team.map((member, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={member.name}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: idx * 0.1}}
|
||||||
|
whileHover={{scale: 1.015}}
|
||||||
|
className="flex flex-col items-center text-center
|
||||||
|
rounded-xl border border-gray-200 dark:border-gray-700
|
||||||
|
shadow-md hover:shadow-lg transition-all p-6"
|
||||||
|
style={{backgroundColor: colors.secondaryBg}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{scale: 1.05}}
|
||||||
|
transition={{type: "spring", stiffness: 300, damping: 20}}
|
||||||
|
className="w-28 h-28 relative mb-4"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={member.picture || fallbackImage}
|
||||||
|
alt={member.name}
|
||||||
|
fill
|
||||||
|
sizes="112px"
|
||||||
|
className="rounded-full object-cover shadow"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="h-px w-8 bg-gray-300 dark:bg-gray-600 my-4"/>
|
||||||
|
|
||||||
|
<div className="text-lg font-semibold" style={{color: colors.primaryText}}>
|
||||||
|
{member.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm mt-1" style={{color: colors.secondaryText}}>
|
||||||
|
{member.role}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamSection;
|
||||||
31
frontend/components/Contact/Contact.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import ContactHero from "@/components/Contact/Section/ContactHero";
|
||||||
|
import ContactFormSection from "@/components/Contact/Section/ContactFormSection";
|
||||||
|
import Section from "@/components/Section";
|
||||||
|
|
||||||
|
const Contact = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.7, ease: "easeOut"}}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<ContactHero/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||||
|
<ContactFormSection/>
|
||||||
|
</Section>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contact;
|
||||||
221
frontend/components/Contact/Section/ContactFormSection.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
|
const ContactFormSection = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
company: "",
|
||||||
|
phone: "",
|
||||||
|
website: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
const hCaptchaSiteKey = isDev
|
||||||
|
? "10000000-ffff-ffff-ffff-000000000001" // hCaptcha test sitekey
|
||||||
|
: "ES_ff59a664dc764f92870bf2c7b4eab7c5";
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setForm({...form, [e.target.name]: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
if (!captchaToken) {
|
||||||
|
setError("Bitte löse das CAPTCHA, um fortzufahren.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const res = await fetch("/api/contact", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({...form, captcha: captchaToken}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setSubmitted(true);
|
||||||
|
setForm({name: "", email: "", company: "", phone: "", website: "", message: ""});
|
||||||
|
} else {
|
||||||
|
const resJson = await res.json();
|
||||||
|
setError(resJson?.error || "Ein Fehler ist aufgetreten. Bitte versuche es später erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-6 sm:px-12 py-20 text-left transition-theme">
|
||||||
|
<motion.h2
|
||||||
|
className="text-2xl sm:text-3xl font-bold mb-2"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
Schreib uns eine Nachricht
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm mb-8 max-w-xl"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir freuen uns über dein Interesse und melden uns schnellstmöglich bei dir zurück.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{submitted ? (
|
||||||
|
<div className="text-green-600 font-semibold text-lg">✅ Deine Nachricht wurde erfolgreich
|
||||||
|
gesendet!</div>
|
||||||
|
) : (
|
||||||
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
label: "Dein Name *",
|
||||||
|
name: "name",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "Max Mustermann"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Deine E-Mail *",
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
required: true,
|
||||||
|
placeholder: "max@example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Firmenname (optional)",
|
||||||
|
name: "company",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
placeholder: "Mustermann GmbH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Telefonnummer (optional)",
|
||||||
|
name: "phone",
|
||||||
|
type: "tel",
|
||||||
|
required: false,
|
||||||
|
placeholder: "+49 123 456789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Webseite (optional)",
|
||||||
|
name: "website",
|
||||||
|
type: "url",
|
||||||
|
required: false,
|
||||||
|
placeholder: "https://..."
|
||||||
|
},
|
||||||
|
].map((field, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={field.name}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: index * 0.1}}
|
||||||
|
>
|
||||||
|
<label className="block font-semibold mb-1" style={{color: colors.primaryText}}>
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={field.type}
|
||||||
|
name={field.name}
|
||||||
|
value={form[field.name as keyof typeof form]}
|
||||||
|
onChange={handleChange}
|
||||||
|
required={field.required}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.6}}
|
||||||
|
>
|
||||||
|
<label className="block font-semibold mb-1" style={{color: colors.primaryText}}>
|
||||||
|
Deine Nachricht *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
value={form.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Worum geht es?"
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="pt-2"
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.7}}
|
||||||
|
>
|
||||||
|
<HCaptcha sitekey={hCaptchaSiteKey} onVerify={setCaptchaToken}/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-600 font-medium pt-2">
|
||||||
|
❌ {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="pt-4 flex justify-end"
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.8}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="px-6 py-3 bg-blue-600 text-white text-sm sm:text-base font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Sende..." : "📩 Nachricht senden"}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactFormSection;
|
||||||
19
frontend/components/Contact/Section/ContactHero.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import SmallHero from "@/components/Helper/SmallHero";
|
||||||
|
|
||||||
|
const ContactHero = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<SmallHero
|
||||||
|
title="Kontakt"
|
||||||
|
subtitle="Du hast Fragen oder möchtest ein Projekt besprechen? Schreib uns!"
|
||||||
|
backgroundImage="/images/contact.png"
|
||||||
|
blurBackground
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactHero;
|
||||||
97
frontend/components/Footer/Footer.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<motion.footer
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.6, ease: 'easeOut'}}
|
||||||
|
className="py-10 transition-theme text-white"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#16171f', // modern dark blue-purple tone
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-[90%] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-8">
|
||||||
|
{/* Logo and description */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl md:text-2xl font-bold text-white">
|
||||||
|
<span className="text-3xl md:text-4xl text-pink-700">R</span>hein Software
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Informationen */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white">Informationen</h3>
|
||||||
|
<ul className="mt-4 space-y-4 text-sm font-semibold text-gray-400">
|
||||||
|
<li>
|
||||||
|
<Link href="/contact">
|
||||||
|
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||||
|
Kontakt
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/*<li>*/}
|
||||||
|
{/* <Link href="/contact">*/}
|
||||||
|
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||||
|
{/* Zahlung und Versand*/}
|
||||||
|
{/* </p>*/}
|
||||||
|
{/* </Link>*/}
|
||||||
|
{/*</li>*/}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rechtliches */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white">Rechtliches</h3>
|
||||||
|
<ul className="mt-4 space-y-4 text-sm font-semibold text-gray-400">
|
||||||
|
{/*<li>*/}
|
||||||
|
{/* <Link href="/legal/terms-of-use">*/}
|
||||||
|
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||||
|
{/* AGB*/}
|
||||||
|
{/* </p>*/}
|
||||||
|
{/* </Link>*/}
|
||||||
|
{/*</li>*/}
|
||||||
|
{/*<li>*/}
|
||||||
|
{/* <Link href="/legal/revocation">*/}
|
||||||
|
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||||
|
{/* Widerruf*/}
|
||||||
|
{/* </p>*/}
|
||||||
|
{/* </Link>*/}
|
||||||
|
{/*</li>*/}
|
||||||
|
<li>
|
||||||
|
<Link href="/legal/privacy">
|
||||||
|
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||||
|
Datenschutz
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/legal/imprint">
|
||||||
|
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||||
|
Impressum
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Section */}
|
||||||
|
<div
|
||||||
|
className="mt-8 border-t border-gray-600 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-400">
|
||||||
|
<p className="text-center md:text-left">
|
||||||
|
© 2025 Rhein Software Development. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
34
frontend/components/Helper/SectionDivider.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const SectionDivider1 = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-20 transition-all duration-500 ease-in-out"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, var(--primary-bg), var(--secondary-bg))`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SectionDivider2 = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-20 transition-all duration-500 ease-in-out"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, var(--secondary-bg), var(--primary-bg))`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SectionDivider3 = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-20 transition-all duration-500 ease-in-out"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, var(--primary-bg), var(--footer-bg))`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
58
frontend/components/Helper/SmallHero.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
|
||||||
|
type SmallHeroProps = {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
backgroundImage?: string;
|
||||||
|
blurBackground?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SmallHero = ({title, subtitle, backgroundImage, blurBackground}: SmallHeroProps) => {
|
||||||
|
const {theme} = useContext(ThemeContext);
|
||||||
|
const colors = themeColors[theme];
|
||||||
|
|
||||||
|
const primaryTextColor = backgroundImage ? "#ffffff" : colors.primaryText;
|
||||||
|
const secondaryTextColor = backgroundImage ? "rgba(255, 255, 255, 0.8)" : "#6B7280"; // Tailwind gray-500
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full py-36 overflow-hidden">
|
||||||
|
{backgroundImage && blurBackground && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center blur-sm scale-[1.05] z-0 will-change-transform"
|
||||||
|
style={{backgroundImage: `url(${backgroundImage})`}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text content */}
|
||||||
|
<div className="relative z-10 px-6 sm:px-12 max-w-5xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-3xl sm:text-4xl font-bold text-left"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.6}}
|
||||||
|
style={{color: primaryTextColor}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h1>
|
||||||
|
{subtitle && (
|
||||||
|
<motion.p
|
||||||
|
className="mt-3 text-lg text-left"
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.6, delay: 0.2}}
|
||||||
|
style={{color: secondaryTextColor}}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SmallHero;
|
||||||
34
frontend/components/Helper/ThemeColors.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const themeColors: Record<
|
||||||
|
"light" | "dark",
|
||||||
|
{
|
||||||
|
primaryBg: string;
|
||||||
|
secondaryBg: string;
|
||||||
|
navBg: string;
|
||||||
|
footerBg: string;
|
||||||
|
primaryText: string;
|
||||||
|
secondaryText: string;
|
||||||
|
inputFieldBg: string;
|
||||||
|
inputBorder: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
light: {
|
||||||
|
primaryBg: "#F3F4F6",
|
||||||
|
secondaryBg: "#eff1f3",
|
||||||
|
navBg: "#F9FAFB",
|
||||||
|
footerBg: "#E5E7EB",
|
||||||
|
primaryText: "#1E293B",
|
||||||
|
secondaryText: "#475569",
|
||||||
|
inputFieldBg: "#ffffff",
|
||||||
|
inputBorder: "#cbd5e1",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primaryBg: "#1A1A23",
|
||||||
|
secondaryBg: "#22222C",
|
||||||
|
navBg: "#2A2A35",
|
||||||
|
footerBg: "#1F1F29",
|
||||||
|
primaryText: "#F0F0F3",
|
||||||
|
secondaryText: "#C0C2CC",
|
||||||
|
inputFieldBg: "#2D2D38",
|
||||||
|
inputBorder: "#4B4B5A",
|
||||||
|
},
|
||||||
|
};
|
||||||
45
frontend/components/Home/Home.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import ContactCTA from "@/components/Home/Sections/ContactCTA";
|
||||||
|
import HomeServices from "@/components/Home/Sections/HomeServices";
|
||||||
|
import TechStack from "@/components/Home/Sections/TechStack";
|
||||||
|
import Section from "@/components/Section";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import Hero from "@/components/Home/Sections/Hero";
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.7, ease: "easeOut"}}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}}>
|
||||||
|
<Hero/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/*<Section style={{backgroundColor: colors.secondaryBg}} shadow>*/}
|
||||||
|
{/* <About/>*/}
|
||||||
|
{/*</Section>*/}
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<HomeServices/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||||
|
<TechStack/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<ContactCTA/>
|
||||||
|
</Section>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
84
frontend/components/Home/Sections/About.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// import Link from 'next/link';
|
||||||
|
// import {FiArrowRight} from 'react-icons/fi';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
import {useThemeColors} from '@/utils/useThemeColors';
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative w-full py-24 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* Title */}
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||||
|
>
|
||||||
|
Über uns
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
|
<div className="p-0 max-w-4xl">
|
||||||
|
<motion.p
|
||||||
|
className="text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir sind Rhein-Software – ein Team, das sich auf individuelle Softwarelösungen und digitale
|
||||||
|
Services spezialisiert hat. Unsere Anwendungen sind technisch solide, skalierbar und
|
||||||
|
durchdacht – gebaut für langfristigen Erfolg.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="mt-6 text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.3}}
|
||||||
|
>
|
||||||
|
Von der ersten Idee bis zum Go-Live begleiten wir Unternehmen und Startups mit einem
|
||||||
|
flexiblen Netzwerk, klarer Kommunikation und einem hohen Anspruch an Qualität.
|
||||||
|
Unsere Lösungen sind intuitiv, effizient – und genau auf deine Anforderungen zugeschnitten.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<motion.div
|
||||||
|
className="mt-10 flex justify-end"
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.5}}
|
||||||
|
>
|
||||||
|
{/*<Link href="/about">*/}
|
||||||
|
{/* <button*/}
|
||||||
|
{/* className="flex items-center gap-2 bg-blue-700 hover:bg-blue-900 text-white font-semibold px-5 py-2 rounded-full shadow-lg transition-all"*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* Mehr über uns <FiArrowRight size={18}/>*/}
|
||||||
|
{/* </button>*/}
|
||||||
|
{/*</Link>*/}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
62
frontend/components/Home/Sections/ContactCTA.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
import {FiArrowRight} from 'react-icons/fi';
|
||||||
|
import {useThemeColors} from '@/utils/useThemeColors';
|
||||||
|
|
||||||
|
type ContactCTAProps = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
buttonLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContactCTA = ({
|
||||||
|
title = "Interesse geweckt?",
|
||||||
|
description = "Lass uns über dein Projekt sprechen. Wir freuen uns darauf, deine Ideen in die Realität umzusetzen.",
|
||||||
|
buttonLabel = "Jetzt Kontakt aufnehmen",
|
||||||
|
}: ContactCTAProps) => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative w-full py-24 overflow-hidden transition-colors duration-700 ease-in-out"
|
||||||
|
style={{backgroundColor: colors.primaryBg, color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-4xl px-6 md:px-10 mx-auto text-center">
|
||||||
|
<motion.h2 className="text-3xl md:text-4xl font-bold">
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="mt-4 text-sm md:text-base max-w-xl mx-auto"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-8 flex justify-center"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.5, delay: 0.3}}
|
||||||
|
>
|
||||||
|
<Link href="/contact">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 text-sm md:text-base font-semibold rounded-full bg-blue-700 hover:bg-blue-900 text-white shadow-md transition-all duration-300"
|
||||||
|
>
|
||||||
|
{buttonLabel} <FiArrowRight size={18}/>
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactCTA;
|
||||||
100
frontend/components/Home/Sections/Hero.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import {Typewriter} from 'react-simple-typewriter';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
|
||||||
|
const Hero = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative w-full pt-[4vh] md:pt-[12vh] h-screen flex flex-col overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--primary-bg)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Background Image */}
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<Image
|
||||||
|
src="/images/home_hero.jpg"
|
||||||
|
alt="Rhein river aerial view"
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
className="blur-md scale-105"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/40"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative z-10 flex justify-center flex-col w-[90%] sm:w-[80%] h-full mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 items-center gap-12">
|
||||||
|
{/* Text Content */}
|
||||||
|
<div>
|
||||||
|
<motion.h1
|
||||||
|
className="text-3xl sm:text-4xl md:text-5xl mt-6 mb-6 font-bold text-white"
|
||||||
|
initial={{opacity: 0, y: 30}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.6, ease: 'easeOut'}}
|
||||||
|
>
|
||||||
|
Rhein-Software Development
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-lg md:text-xl text-gray-400"
|
||||||
|
initial={{opacity: 0, y: 30}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.6, delay: 0.2, ease: 'easeOut'}}
|
||||||
|
>
|
||||||
|
Digitale Lösungen für dein Unternehmen.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="mt-4 text-lg md:text-xl font-semibold text-white"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
animate={{opacity: 1}}
|
||||||
|
transition={{delay: 0.9, duration: 0.6}}
|
||||||
|
>
|
||||||
|
<Typewriter
|
||||||
|
words={['Beratung', 'Entwicklung', 'Wartung', 'Fehlerbehebung']}
|
||||||
|
loop={true}
|
||||||
|
cursor
|
||||||
|
cursorStyle="_"
|
||||||
|
typeSpeed={60}
|
||||||
|
deleteSpeed={40}
|
||||||
|
delaySpeed={1500}
|
||||||
|
/>
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating Image */}
|
||||||
|
<motion.div
|
||||||
|
className="hidden lg:block"
|
||||||
|
initial={{opacity: 0, y: 30}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.8, ease: 'easeOut', delay: 0.3}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{y: [0, -10, 0]}}
|
||||||
|
transition={{
|
||||||
|
duration: 4,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
className="animate-float"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/images/hero.png"
|
||||||
|
alt="hero graphic"
|
||||||
|
width={700}
|
||||||
|
height={700}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
105
frontend/components/Home/Sections/HomeServices.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {FiServer, FiTool, FiMonitor, FiZap, FiArrowRight} from 'react-icons/fi';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
import {useThemeColors} from '@/utils/useThemeColors';
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Beratung',
|
||||||
|
icon: <FiMonitor size={24}/>,
|
||||||
|
description:
|
||||||
|
'Strategische und technische Beratung rund um digitale Produkte und Prozesse. Wir analysieren bestehende Systeme, identifizieren Potenziale und helfen dir, die passende Architektur für dein Projekt zu finden.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Entwicklung',
|
||||||
|
icon: <FiZap size={24}/>,
|
||||||
|
description:
|
||||||
|
'Individuelle Softwareentwicklung – skalierbar, wartbar, zukunftssicher. Ob Web-App, API oder internes Tool: Wir setzen moderne Technologien ein, um genau die Lösung zu bauen, die du brauchst.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Managed Services',
|
||||||
|
icon: <FiServer size={24}/>,
|
||||||
|
description:
|
||||||
|
'Wir betreuen Infrastruktur, Server und Systeme – verlässlich und performant. Unser Team kümmert sich um Hosting, Monitoring, Backups und sorgt für einen reibungslosen Betrieb deiner Plattform.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fehlerbehebung',
|
||||||
|
icon: <FiTool size={24}/>,
|
||||||
|
description:
|
||||||
|
'Schnelle Hilfe bei Bugs, Performance-Problemen oder Sicherheitslücken. Wir analysieren Probleme, beheben sie gezielt und sorgen dafür, dass dein System wieder stabil läuft – langfristig und zuverlässig.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const HomeServices = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="w-full py-24 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{backgroundColor: colors.primaryBg}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto" style={{color: colors.primaryText}}>
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||||
|
>
|
||||||
|
Leistungen
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-10 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={service.title}
|
||||||
|
className="p-6 rounded-xl border shadow-md transition-colors duration-700 ease-in-out"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondaryBg,
|
||||||
|
borderColor: colors.secondaryBg,
|
||||||
|
color: colors.primaryText,
|
||||||
|
}}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.03,
|
||||||
|
boxShadow: '0px 10px 20px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
initial={{opacity: 0, y: 30}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: index * 0.1}}
|
||||||
|
>
|
||||||
|
<div className="mb-3 text-blue-600">{service.icon}</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">{service.title}</h3>
|
||||||
|
<p className="text-sm leading-relaxed transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}>
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-10 flex justify-end"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
whileInView={{opacity: 1}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.3}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/services"
|
||||||
|
className="text-sm font-semibold text-blue-600 hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Weitere Leistungen <FiArrowRight size={16}/>
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeServices;
|
||||||
139
frontend/components/Home/Sections/TechStack.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import {motion} from 'framer-motion';
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const techStack = {
|
||||||
|
row1: [
|
||||||
|
{
|
||||||
|
category: 'Programmiersprachen & Frameworks – Backend',
|
||||||
|
items: [
|
||||||
|
{id: 'java', label: 'Java'},
|
||||||
|
{id: 'dart', label: 'Dart'},
|
||||||
|
{id: 'kotlin', label: 'Kotlin'},
|
||||||
|
{id: 'spring', label: 'Spring'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Programmiersprachen & Frameworks – Frontend',
|
||||||
|
items: [
|
||||||
|
{id: 'html', label: 'HTML'},
|
||||||
|
{id: 'css', label: 'CSS'},
|
||||||
|
{id: 'bootstrap', label: 'Bootstrap'},
|
||||||
|
{id: 'nextjs', label: 'Next.js'},
|
||||||
|
{id: 'typescript', label: 'TypeScript'},
|
||||||
|
{id: 'flutter', label: 'Flutter'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
row2: [
|
||||||
|
{
|
||||||
|
category: 'Betriebssysteme',
|
||||||
|
items: [
|
||||||
|
{id: 'macos', label: 'macOS'},
|
||||||
|
{id: 'debian', label: 'Debian'},
|
||||||
|
{id: 'ubuntu', label: 'Ubuntu'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Version Control & Collaboration',
|
||||||
|
items: [
|
||||||
|
{id: 'gitlab', label: 'GitLab'},
|
||||||
|
{id: 'outline', label: 'Outline'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'DevOps & Infrastruktur',
|
||||||
|
items: [
|
||||||
|
{id: 'gitlab-ci', label: 'GitLab CI'},
|
||||||
|
{id: 'docker', label: 'Docker'},
|
||||||
|
{id: 'proxmox', label: 'Proxmox'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const TechStack = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="w-full py-20 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{backgroundColor: colors.secondaryBg}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto" style={{color: colors.primaryText}}>
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||||
|
>
|
||||||
|
Technologien
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm md:text-base mb-10 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
>
|
||||||
|
Mit diesen Technologien realisieren wir moderne, leistungsstarke Softwarelösungen.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
{techStack.row1.map((group, index) => (
|
||||||
|
<TechCard key={group.category} group={group} delay={index * 0.2} colors={colors}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{techStack.row2.map((group, index) => (
|
||||||
|
<TechCard key={group.category} group={group} delay={index * 0.2 + 0.4} colors={colors}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TechCard = ({
|
||||||
|
group,
|
||||||
|
delay,
|
||||||
|
colors,
|
||||||
|
}: {
|
||||||
|
group: { category: string; items: { id: string; label: string }[] };
|
||||||
|
delay: number;
|
||||||
|
colors: ReturnType<typeof useThemeColors>;
|
||||||
|
}) => (
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-lg border shadow-md transition-colors duration-700 ease-in-out"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.primaryBg,
|
||||||
|
borderColor: colors.primaryBg,
|
||||||
|
color: colors.primaryText,
|
||||||
|
}}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
whileHover={{scale: 1.03, boxShadow: '0 10px 20px rgba(0,0,0,0.1)'}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay}}
|
||||||
|
>
|
||||||
|
<h3 className="text-base font-semibold mb-4">{group.category}</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{group.items.map(({id, label}) => (
|
||||||
|
<div key={id} className="flex flex-col items-center text-center">
|
||||||
|
<Image src={`/images/svg/${id}.svg`} alt={label} width={32} height={32} className="object-contain"/>
|
||||||
|
<span className="text-[10px] mt-1 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TechStack;
|
||||||
131
frontend/components/Legal/Imprint/ImprintComp.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
|
||||||
|
const fadeInUp = {
|
||||||
|
hidden: {opacity: 0, y: 30},
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
delay: i * 0.2,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImprintComp = () => {
|
||||||
|
const {theme} = useContext(ThemeContext);
|
||||||
|
const colors = themeColors[theme];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="overflow-hidden transition-colors duration-500"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
{/* Imprint Content */}
|
||||||
|
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12 space-y-10 text-base leading-relaxed">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "Impressum",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
Thatsaphorn Atchariyaphap<br/>
|
||||||
|
Rhein-Software (Einzelunternehmer)<br/>
|
||||||
|
Mühlenstrasse 13<br/>
|
||||||
|
79664 Wehr
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Kontakt",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
Telefon: +49 (0) 151 24003632<br/>
|
||||||
|
E-Mail:{" "}
|
||||||
|
<a
|
||||||
|
href="mailto:contact@rhein-software.dev"
|
||||||
|
className="underline text-blue-500"
|
||||||
|
>
|
||||||
|
contact@rhein-software.dev
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "EU-Streitschlichtung",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
Die Europäische Kommission stellt eine Plattform zur
|
||||||
|
Online-Streitbeilegung (OS) bereit:{" "}
|
||||||
|
<a
|
||||||
|
href="https://ec.europa.eu/consumers/odr/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline text-blue-500"
|
||||||
|
>
|
||||||
|
https://ec.europa.eu/consumers/odr/
|
||||||
|
</a>
|
||||||
|
.<br/>
|
||||||
|
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
"Verbraucherstreitbeilegung / Universalschlichtungsstelle",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
Wir sind nicht bereit oder verpflichtet, an
|
||||||
|
Streitbeilegungsverfahren vor einer
|
||||||
|
Verbraucherschlichtungsstelle teilzunehmen.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
].map((section, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={section.title}
|
||||||
|
custom={i}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={fadeInUp}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold">{section.title}</h2>
|
||||||
|
<motion.div
|
||||||
|
className="w-6 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
<p>{section.content}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
custom={4}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={fadeInUp}
|
||||||
|
>
|
||||||
|
Quelle:{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.e-recht24.de"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
www.e-recht24.de
|
||||||
|
</a>
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImprintComp;
|
||||||
145
frontend/components/Legal/Privacy/PrivacyComp.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
|
||||||
|
const fadeInUp = {
|
||||||
|
hidden: {opacity: 0, y: 30},
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
delay: i * 0.2,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrivacyComp = () => {
|
||||||
|
const {theme} = useContext(ThemeContext);
|
||||||
|
const colors = themeColors[theme];
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: "1. Datenschutz auf einen Blick",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen
|
||||||
|
Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit
|
||||||
|
denen Sie persönlich identifiziert werden können.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten
|
||||||
|
finden Sie im Abschnitt „Hinweis zur Verantwortlichen Stelle“.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2. Allgemeine Hinweise und Pflichtinformationen",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
|
||||||
|
Datenschutzvorschriften sowie dieser Datenschutzerklärung. Wir weisen darauf hin, dass die
|
||||||
|
Datenübertragung im Internet Sicherheitslücken aufweisen kann.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Verantwortlich für die Datenverarbeitung:
|
||||||
|
<br/>
|
||||||
|
Rhein-Software Development
|
||||||
|
<br/>
|
||||||
|
Mühlenstrasse 13
|
||||||
|
<br/>
|
||||||
|
79664 Wehr
|
||||||
|
<br/><br/>
|
||||||
|
Telefon: +49 (0) 151 24003632
|
||||||
|
<br/>
|
||||||
|
E-Mail:{" "}
|
||||||
|
<a
|
||||||
|
href="mailto:contact@rhein-software.dev"
|
||||||
|
className="underline text-blue-500"
|
||||||
|
>
|
||||||
|
contact@rhein-software.dev
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3. Ihre Rechte",
|
||||||
|
content: (
|
||||||
|
<p>
|
||||||
|
Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung,
|
||||||
|
Datenübertragbarkeit sowie auf Widerspruch gegen die Verarbeitung Ihrer Daten. Zudem haben Sie
|
||||||
|
ein Beschwerderecht bei der zuständigen Aufsichtsbehörde.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4. Analyse-Tools und Cookies",
|
||||||
|
content: (
|
||||||
|
<p>
|
||||||
|
Beim Besuch dieser Website kann Ihr Surfverhalten statistisch ausgewertet werden. Das geschieht
|
||||||
|
vor allem mit sogenannten Analyseprogrammen und Cookies. Detaillierte Informationen hierzu
|
||||||
|
entnehmen Sie bitte unserer vollständigen Datenschutzerklärung.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "5. Sicherheit",
|
||||||
|
content: (
|
||||||
|
<p>
|
||||||
|
Diese Seite nutzt eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen
|
||||||
|
Sie an der Adresszeile des Browsers („https://“) und dem Schloss-Symbol.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Quelle",
|
||||||
|
content: (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Quelle:{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.e-recht24.de"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
www.e-recht24.de
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="overflow-hidden transition-colors duration-500"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
{/* Privacy Content */}
|
||||||
|
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12 space-y-10 text-base leading-relaxed">
|
||||||
|
{sections.map((section, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={section.title}
|
||||||
|
custom={i}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{once: true, amount: 0.2}}
|
||||||
|
variants={fadeInUp}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{section.title}</h2>
|
||||||
|
<div className="space-y-4">{section.content}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivacyComp;
|
||||||
98
frontend/components/Legal/RevocationComp.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import SmallHero from "@/components/Helper/SmallHero";
|
||||||
|
import React, {useContext, useEffect} from "react";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
import AOS from "aos";
|
||||||
|
|
||||||
|
const RevocationComp = () => {
|
||||||
|
const {theme} = useContext(ThemeContext);
|
||||||
|
const colors = themeColors[theme];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({
|
||||||
|
duration: 1000,
|
||||||
|
easing: "ease",
|
||||||
|
once: true,
|
||||||
|
anchorPlacement: "top-bottom",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden transition-colors duration-500"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="mt-[10vh]">
|
||||||
|
<SmallHero
|
||||||
|
title="Widerruf"
|
||||||
|
subtitle=""
|
||||||
|
backgroundImage="/images/contact.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-center"
|
||||||
|
data-aos="fade-up"
|
||||||
|
data-aos-delay="400"
|
||||||
|
>
|
||||||
|
Schreib uns eine Nachricht
|
||||||
|
</h2>
|
||||||
|
<p data-aos="fade-up" data-aos-delay="600"
|
||||||
|
className="text-center mt-3 text-[var(--secondary-text)]">
|
||||||
|
Wir melden uns schnellstmöglich bei dir!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form className="mt-8 max-w-2xl mx-auto space-y-6">
|
||||||
|
{/* Name & Email */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{["Dein Name", "Deine E-Mail"].map((label, index) => (
|
||||||
|
<div key={index} data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<label className="block font-semibold">{label}</label>
|
||||||
|
<input
|
||||||
|
type={index === 0 ? "text" : "email"}
|
||||||
|
placeholder={index === 0 ? "Max Mustermann" : "max@example.com"}
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<label className="block font-semibold">Deine Nachricht</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Schreibe deine Nachricht..."
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-3 bg-blue-600 text-white text-lg font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all"
|
||||||
|
>
|
||||||
|
📩 Nachricht senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);};
|
||||||
|
|
||||||
|
export default RevocationComp;
|
||||||
99
frontend/components/Legal/TermsOfUseComp.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import SmallHero from "@/components/Helper/SmallHero";
|
||||||
|
import React, {useContext, useEffect} from "react";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||||
|
import AOS from "aos";
|
||||||
|
|
||||||
|
const TermsOfUseComp = () => {
|
||||||
|
const {theme} = useContext(ThemeContext);
|
||||||
|
const colors = themeColors[theme];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({
|
||||||
|
duration: 1000,
|
||||||
|
easing: "ease",
|
||||||
|
once: true,
|
||||||
|
anchorPlacement: "top-bottom",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden transition-colors duration-500"
|
||||||
|
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="mt-[10vh]">
|
||||||
|
<SmallHero
|
||||||
|
title="AGB"
|
||||||
|
subtitle=""
|
||||||
|
backgroundImage="/images/contact.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-center"
|
||||||
|
data-aos="fade-up"
|
||||||
|
data-aos-delay="400"
|
||||||
|
>
|
||||||
|
Schreib uns eine Nachricht
|
||||||
|
</h2>
|
||||||
|
<p data-aos="fade-up" data-aos-delay="600"
|
||||||
|
className="text-center mt-3 text-[var(--secondary-text)]">
|
||||||
|
Wir melden uns schnellstmöglich bei dir!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form className="mt-8 max-w-2xl mx-auto space-y-6">
|
||||||
|
{/* Name & Email */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{["Dein Name", "Deine E-Mail"].map((label, index) => (
|
||||||
|
<div key={index} data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<label className="block font-semibold">{label}</label>
|
||||||
|
<input
|
||||||
|
type={index === 0 ? "text" : "email"}
|
||||||
|
placeholder={index === 0 ? "Max Mustermann" : "max@example.com"}
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<label className="block font-semibold">Deine Nachricht</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Schreibe deine Nachricht..."
|
||||||
|
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.inputFieldBg,
|
||||||
|
border: `1px solid ${colors.inputBorder}`,
|
||||||
|
color: colors.primaryText
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-3 bg-blue-600 text-white text-lg font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all"
|
||||||
|
>
|
||||||
|
📩 Nachricht senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TermsOfUseComp;
|
||||||
110
frontend/components/Navbar/DesktopNav.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {usePathname} from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, {useContext, useEffect, useState} from "react";
|
||||||
|
import {HiBars3BottomRight} from "react-icons/hi2";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import {navLinks} from "@/constant/Constant";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
openNav: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Nav = ({openNav}: Props) => {
|
||||||
|
const [navBg, setNavBg] = useState(false);
|
||||||
|
const [navHeight, setNavHeight] = useState("h-[10vh]");
|
||||||
|
const [contentSize, setContentSize] = useState("text-base md:text-lg");
|
||||||
|
const [buttonSize, setButtonSize] = useState("md:px-6 md:py-2 px-4 py-1 text-sm");
|
||||||
|
|
||||||
|
const {theme, toggleTheme} = useContext(ThemeContext);
|
||||||
|
const colors = useThemeColors();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const navColorClass = theme === "dark" || !navBg ? "text-white" : "text-black";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
if (window.scrollY >= 90) {
|
||||||
|
setNavBg(true);
|
||||||
|
setNavHeight("h-[8vh]");
|
||||||
|
setContentSize("text-sm md:text-base");
|
||||||
|
setButtonSize("md:px-5 md:py-1.5 px-3 py-1 text-xs");
|
||||||
|
} else {
|
||||||
|
setNavBg(false);
|
||||||
|
setNavHeight("h-[10vh]");
|
||||||
|
setContentSize("text-base md:text-lg");
|
||||||
|
setButtonSize("md:px-6 md:py-2 px-4 py-1 text-sm");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handler);
|
||||||
|
return () => window.removeEventListener("scroll", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed w-full transition-all duration-300 ease-in-out ${navHeight} z-[1000] ${
|
||||||
|
navBg ? "shadow-md" : ""
|
||||||
|
}`}
|
||||||
|
style={{backgroundColor: navBg ? colors.navBg : "transparent"}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center h-full justify-between w-[90%] xl:w-[80%] mx-auto">
|
||||||
|
<Link href="/">
|
||||||
|
<h1 className={`${contentSize} font-bold cursor-pointer ${navColorClass}`}>
|
||||||
|
<span className="text-lg md:text-xl text-pink-700">R</span>hein Software
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="hidden lg:flex items-center space-x-6">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link href={link.url} key={link.id}>
|
||||||
|
<p className={`relative group ${contentSize} uppercase ${getNavLinkClasses(pathname === link.url, navBg, theme, navColorClass)}`}>
|
||||||
|
{link.label}
|
||||||
|
{pathname !== link.url && (
|
||||||
|
<span
|
||||||
|
className="absolute bottom-0 left-0 w-full h-[2px] bg-current transform transition-transform duration-300 origin-right scale-x-0 group-hover:scale-x-100"/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Link href="/contact">
|
||||||
|
<button
|
||||||
|
className={`${buttonSize} text-white font-semibold bg-blue-700 hover:bg-blue-900 rounded-full`}>
|
||||||
|
Kontakt
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className={`w-7 h-7 flex items-center justify-center rounded-full ${navColorClass}`}
|
||||||
|
style={{backgroundColor: colors.secondaryBg}}
|
||||||
|
>
|
||||||
|
{theme === "dark" ? "🌙" : "☀️"}
|
||||||
|
</button>
|
||||||
|
<HiBars3BottomRight
|
||||||
|
onClick={openNav}
|
||||||
|
className={`w-6 h-6 cursor-pointer lg:hidden ${navColorClass}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNavLinkClasses = (
|
||||||
|
isActive: boolean,
|
||||||
|
navBg: boolean,
|
||||||
|
theme: string,
|
||||||
|
navColorClass: string
|
||||||
|
): string => {
|
||||||
|
if (isActive) return !navBg ? "text-white font-bold" : `${navColorClass} font-bold`;
|
||||||
|
if (!navBg) return "text-white font-medium";
|
||||||
|
return theme === "dark" ? "text-gray-300 font-medium" : "text-gray-700 font-medium";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
62
frontend/components/Navbar/MobileNav.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {navLinks} from "@/constant/Constant";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {CgClose} from "react-icons/cg";
|
||||||
|
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
showNav: boolean;
|
||||||
|
closeNav: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileNav = ({closeNav, showNav}: Props) => {
|
||||||
|
const navOpen = showNav ? "translate-y-0 opacity-100" : "-translate-y-20 opacity-0 pointer-events-none";
|
||||||
|
const {theme, toggleTheme} = useContext(ThemeContext);
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
const textClass = theme === "dark" ? "text-white" : "text-black";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-[10000] transition-opacity duration-500 ${
|
||||||
|
showNav ? "opacity-60 bg-black" : "opacity-0 pointer-events-none"
|
||||||
|
}`}
|
||||||
|
onClick={closeNav}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 left-0 w-full z-[10006] transform ${navOpen} transition-all duration-500 ease-in-out shadow-md rounded-b-2xl`}
|
||||||
|
style={{backgroundColor: colors.navBg}}
|
||||||
|
>
|
||||||
|
<div className={`flex flex-col items-center justify-center py-8 space-y-4 px-4 relative ${textClass}`}>
|
||||||
|
<CgClose
|
||||||
|
onClick={closeNav}
|
||||||
|
className={`absolute top-4 right-6 sm:right-8 sm:w-7 sm:h-7 w-6 h-6 cursor-pointer p-1 ${textClass}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link href={link.url} key={link.id}>
|
||||||
|
<p className="nav__link uppercase text-[14px] sm:text-[16px] border-b pb-1 border-gray-400 transition-all duration-300 ease-in-out hover:scale-105">
|
||||||
|
{link.label}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className={`mt-4 w-8 h-8 flex items-center justify-center rounded-full border border-gray-400 transition-all duration-300 ${textClass}`}
|
||||||
|
style={{backgroundColor: colors.secondaryBg}}
|
||||||
|
>
|
||||||
|
{theme === "dark" ? "🌙" : "☀️"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileNav;
|
||||||
24
frontend/components/Navbar/Nav.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import DesktopNav from "./DesktopNav";
|
||||||
|
import MobileNav from "./MobileNav";
|
||||||
|
|
||||||
|
const Nav = () => {
|
||||||
|
const [showNav, setShowNav] = useState(false);
|
||||||
|
const handleNavShow = () => {
|
||||||
|
setShowNav(true);
|
||||||
|
};
|
||||||
|
const handleNavHide = () => {
|
||||||
|
setShowNav(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DesktopNav openNav={handleNavShow}/>
|
||||||
|
<MobileNav showNav={showNav} closeNav={handleNavHide}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
34
frontend/components/Section.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {CSSProperties} from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
shadow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Section: React.FC<SectionProps> = ({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
style,
|
||||||
|
shadow = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={clsx("relative transition-colors duration-500", className)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div className="relative z-10">{children}</div>
|
||||||
|
{shadow && (
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-0 w-full h-16 pointer-events-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Section;
|
||||||
81
frontend/components/Services/Section/OverviewTabs.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import {motion, AnimatePresence} from 'framer-motion';
|
||||||
|
import {FiZap, FiMonitor, FiServer, FiTool} from 'react-icons/fi';
|
||||||
|
import Development from "@/components/Services/Section/overview/Development";
|
||||||
|
import Consulting from "@/components/Services/Section/overview/Consulting";
|
||||||
|
import ManagedServices from "@/components/Services/Section/overview/ManagedServices";
|
||||||
|
import BugFixing from "@/components/Services/Section/overview/BugFixing";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{key: 'entwicklung', label: 'Entwicklung', icon: <FiZap size={20}/>},
|
||||||
|
{key: 'beratung', label: 'Beratung', icon: <FiMonitor size={20}/>},
|
||||||
|
{key: 'services', label: 'Managed Services', icon: <FiServer size={20}/>},
|
||||||
|
{key: 'support', label: 'Fehlerbehebung', icon: <FiTool size={20}/>},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabContent: Record<string, React.ReactNode> = {
|
||||||
|
entwicklung: <Development/>,
|
||||||
|
beratung: <Consulting/>,
|
||||||
|
services: <ManagedServices/>,
|
||||||
|
support: <BugFixing/>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OverviewTabs = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState("entwicklung");
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl mx-auto px-6 py-16 transition-theme text-left">
|
||||||
|
<h2 className="text-3xl font-bold mb-2" style={{color: colors.primaryText}}>
|
||||||
|
Was wir tun
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="w-12 h-[2px] mb-6 bg-amber-500"
|
||||||
|
initial={{opacity: 0, x: -20}}
|
||||||
|
whileInView={{opacity: 1, x: 0}}
|
||||||
|
viewport={{once: true}}
|
||||||
|
transition={{duration: 0.4, delay: 0.1}}
|
||||||
|
/>
|
||||||
|
<p className="text-sm mb-10" style={{color: colors.secondaryText}}>
|
||||||
|
In diesem Abschnitt geben wir dir einen Überblick über unsere zentralen Leistungen – von der technischen
|
||||||
|
Entwicklung über Beratung bis hin zu Betrieb und Support.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-4 mb-10">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className="flex-1 min-w-[150px] flex items-center justify-start gap-2 px-4 py-2 rounded-full border text-sm font-medium transition-all"
|
||||||
|
style={{
|
||||||
|
color: activeTab === tab.key ? '#ffffff' : colors.primaryText,
|
||||||
|
backgroundColor: activeTab === tab.key ? '#1D4ED8' : 'transparent',
|
||||||
|
borderColor: activeTab === tab.key ? '#1D4ED8' : colors.inputBorder,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeTab}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
exit={{opacity: 0, y: -10}}
|
||||||
|
transition={{duration: 0.4}}
|
||||||
|
>
|
||||||
|
{tabContent[activeTab]}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewTabs;
|
||||||
19
frontend/components/Services/Section/ServiceHero.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import SmallHero from "@/components/Helper/SmallHero";
|
||||||
|
|
||||||
|
const ServiceHero = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<SmallHero
|
||||||
|
title="Unsere Leistungen"
|
||||||
|
subtitle="Wir bieten maßgeschneiderte Lösungen – von der Beratung bis zum Betrieb."
|
||||||
|
backgroundImage="/images/contact.png"
|
||||||
|
blurBackground
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceHero;
|
||||||
59
frontend/components/Services/Section/overview/BugFixing.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const BugFixing = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<motion.h2
|
||||||
|
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
🐞 Fehlerbehebung & Optimierung
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm font-medium leading-7"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir analysieren und beheben Fehler in bestehenden Systemen, optimieren die Performance und sorgen
|
||||||
|
dafür, dass deine Software stabil und zuverlässig läuft.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.h3
|
||||||
|
className="mt-8 text-lg font-semibold"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
whileInView={{opacity: 1}}
|
||||||
|
transition={{duration: 0.4, delay: 0.2}}
|
||||||
|
>
|
||||||
|
🔎 Fokusbereiche
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
>
|
||||||
|
<li>Debugging & Troubleshooting</li>
|
||||||
|
<li>Performance-Analyse</li>
|
||||||
|
<li>Refactoring von Legacy-Code</li>
|
||||||
|
<li>Stabilitäts- und Sicherheitsupdates</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BugFixing;
|
||||||
60
frontend/components/Services/Section/overview/Consulting.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const Consulting = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<motion.h2
|
||||||
|
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
🧠 Technische Beratung
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm font-medium leading-7"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
whileInView={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir unterstützen dich dabei, technische Entscheidungen fundiert zu treffen – von der Auswahl
|
||||||
|
geeigneter Technologien bis hin zur Planung skalierbarer Architekturen. Gemeinsam finden wir den
|
||||||
|
effizientesten Weg von der Idee bis zur Umsetzung – praxisnah, zielgerichtet und verständlich.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.h3
|
||||||
|
className="mt-8 text-lg font-semibold"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
whileInView={{opacity: 1}}
|
||||||
|
transition={{duration: 0.4, delay: 0.2}}
|
||||||
|
>
|
||||||
|
🔍 Themenbereiche
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
>
|
||||||
|
<li>Software-Architektur & Microservices</li>
|
||||||
|
<li>Technologie- und Framework-Auswahl</li>
|
||||||
|
<li>Prototyping & Machbarkeitsanalysen</li>
|
||||||
|
<li>Projektplanung und agile Methodik</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Consulting;
|
||||||
210
frontend/components/Services/Section/overview/Development.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const techStack = {
|
||||||
|
row1: [
|
||||||
|
{
|
||||||
|
category: 'Programmiersprachen & Frameworks – Backend',
|
||||||
|
items: [
|
||||||
|
{id: 'java', label: 'Java'},
|
||||||
|
{id: 'dart', label: 'Dart'},
|
||||||
|
{id: 'kotlin', label: 'Kotlin'},
|
||||||
|
{id: 'spring', label: 'Spring'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Programmiersprachen & Frameworks – Frontend',
|
||||||
|
items: [
|
||||||
|
{id: 'html', label: 'HTML'},
|
||||||
|
{id: 'css', label: 'CSS'},
|
||||||
|
{id: 'bootstrap', label: 'Bootstrap'},
|
||||||
|
{id: 'nextjs', label: 'Next.js'},
|
||||||
|
{id: 'typescript', label: 'TypeScript'},
|
||||||
|
{id: 'flutter', label: 'Flutter'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
row2: [
|
||||||
|
{
|
||||||
|
category: 'Betriebssysteme',
|
||||||
|
items: [
|
||||||
|
{id: 'macos', label: 'macOS'},
|
||||||
|
{id: 'debian', label: 'Debian'},
|
||||||
|
{id: 'ubuntu', label: 'Ubuntu'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Version Control & Collaboration',
|
||||||
|
items: [
|
||||||
|
{id: 'gitlab', label: 'GitLab'},
|
||||||
|
{id: 'outline', label: 'Outline'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'DevOps & Infrastruktur',
|
||||||
|
items: [
|
||||||
|
{id: 'gitlab-ci', label: 'GitLab CI'},
|
||||||
|
{id: 'docker', label: 'Docker'},
|
||||||
|
{id: 'proxmox', label: 'Proxmox'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const TechCard = ({group}: {
|
||||||
|
group: { category: string; items: { id: string; label: string }[] };
|
||||||
|
}) => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.4}}
|
||||||
|
className="p-4 rounded-lg border shadow-md transition-colors duration-700 ease-in-out"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.primaryBg,
|
||||||
|
borderColor: colors.primaryBg,
|
||||||
|
color: colors.primaryText,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 className="text-base font-semibold mb-4">{group.category}</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{group.items.map(({id, label}) => (
|
||||||
|
<div key={id} className="flex flex-col items-center text-center">
|
||||||
|
<Image
|
||||||
|
src={`/images/svg/${id}.svg`}
|
||||||
|
alt={label}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="text-[10px] mt-1 transition-colors duration-700 ease-in-out"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Development = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<motion.h2
|
||||||
|
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
💻 Full-Stack Entwicklung
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm font-medium leading-7"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir entwickeln individuelle Softwarelösungen – von der nativen Mobile-App über moderne Webseiten bis
|
||||||
|
hin zu internen Tools.
|
||||||
|
Unser Fokus liegt auf skalierbaren Architekturen, performanten Frontends und wartbaren Backends.
|
||||||
|
<br/><br/>
|
||||||
|
Egal ob API-Entwicklung, Admin-Dashboard oder komplexe Plattform – wir setzen moderne Technologien
|
||||||
|
gezielt ein, um robuste, zukunftssichere Anwendungen zu realisieren.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-6 text-sm font-medium space-y-3 pl-2"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.3}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">🚀</span>
|
||||||
|
<p>Native Mobile-Apps mit Flutter</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">🌐</span>
|
||||||
|
<p>Webseiten & Web-Portale mit Next.js</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">🧩</span>
|
||||||
|
<p>Skalierbare Backends mit Spring Boot</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">📊</span>
|
||||||
|
<p>Individuelle Dashboards & Admin-Panels</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">🔌</span>
|
||||||
|
<p>API-Entwicklung (REST & GraphQL)</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">⚙️</span>
|
||||||
|
<p>Automatisierte interne Tools</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">📦</span>
|
||||||
|
<p>CI/CD & Container mit GitLab CI & Docker</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h3
|
||||||
|
className="mt-10 text-lg font-semibold"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
animate={{opacity: 1}}
|
||||||
|
transition={{duration: 0.4, delay: 0.2}}
|
||||||
|
>
|
||||||
|
🔧 Unser Tech Stack im Überblick
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm font-medium mb-4"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Mit diesem Stack entwickeln wir robuste, moderne Softwarelösungen – abgestimmt auf deine
|
||||||
|
Anforderungen.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
animate={{opacity: 1}}
|
||||||
|
transition={{duration: 0.5, delay: 0.1}}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
{techStack.row1.map((group) => (
|
||||||
|
<TechCard key={group.category} group={group}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{techStack.row2.map((group) => (
|
||||||
|
<TechCard key={group.category} group={group}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Development;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
|
||||||
|
const ManagedServices = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
|
||||||
|
style={{color: colors.primaryText}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<motion.h2
|
||||||
|
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5}}
|
||||||
|
>
|
||||||
|
🛠️ Managed Services
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-sm font-medium leading-7"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.2}}
|
||||||
|
>
|
||||||
|
Wir übernehmen den Betrieb und die Wartung deiner Anwendungen – zuverlässig, sicher und skalierbar.
|
||||||
|
So kannst du dich voll auf dein Geschäft konzentrieren.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.h3
|
||||||
|
className="mt-8 text-lg font-semibold"
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
animate={{opacity: 1}}
|
||||||
|
transition={{duration: 0.4, delay: 0.2}}
|
||||||
|
>
|
||||||
|
🧰 Leistungen
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<motion.ul
|
||||||
|
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||||
|
style={{color: colors.secondaryText}}
|
||||||
|
initial={{opacity: 0, y: 10}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.5, delay: 0.3}}
|
||||||
|
>
|
||||||
|
<li>Monitoring & Logging</li>
|
||||||
|
<li>Security Updates & Wartung</li>
|
||||||
|
<li>Cloud Deployment & Hosting</li>
|
||||||
|
<li>24/7 Systemüberwachung</li>
|
||||||
|
</motion.ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManagedServices;
|
||||||
38
frontend/components/Services/Services.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useThemeColors} from "@/utils/useThemeColors";
|
||||||
|
import ServiceHero from "@/components/Services/Section/ServiceHero";
|
||||||
|
import OverviewTabs from "@/components/Services/Section/OverviewTabs";
|
||||||
|
import ContactCTA from "@/components/Home/Sections/ContactCTA";
|
||||||
|
import Section from "@/components/Section";
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const colors = useThemeColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
transition={{duration: 0.7, ease: "easeOut"}}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<ServiceHero/>
|
||||||
|
</Section>
|
||||||
|
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||||
|
<OverviewTabs/>
|
||||||
|
</Section>
|
||||||
|
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||||
|
<ContactCTA
|
||||||
|
title="Nichts Passendes gefunden?"
|
||||||
|
description="Nimm einfach Kontakt mit uns auf – gemeinsam finden wir die passende Lösung für dein Vorhaben."
|
||||||
|
buttonLabel="Kontakt aufnehmen"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
50
frontend/components/provider/ThemeProvider.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {createContext, useEffect, useState} from "react";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
type ThemeType = "light" | "dark";
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<{
|
||||||
|
theme: ThemeType;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}>({
|
||||||
|
theme: "light",
|
||||||
|
toggleTheme: () => {
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ThemeProvider = ({children}: { children: React.ReactNode }) => {
|
||||||
|
const [theme, setTheme] = useState<ThemeType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = Cookies.get("theme") as ThemeType | undefined;
|
||||||
|
if (saved === "dark" || saved === "light") {
|
||||||
|
setTheme(saved);
|
||||||
|
} else {
|
||||||
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
const defaultTheme: ThemeType = prefersDark ? "dark" : "light";
|
||||||
|
setTheme(defaultTheme);
|
||||||
|
Cookies.set("theme", defaultTheme, {expires: 365});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!theme) return;
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const next = theme === "dark" ? "light" : "dark";
|
||||||
|
setTheme(next);
|
||||||
|
Cookies.set("theme", next, {expires: 365});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!theme) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{theme, toggleTheme}}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
frontend/constant/Constant.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const navLinks = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: '/',
|
||||||
|
label: 'Start',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
url: '/about',
|
||||||
|
label: 'Über Uns',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
url: '/services',
|
||||||
|
label: 'Leistungen',
|
||||||
|
}
|
||||||
|
];
|
||||||
28
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {dirname} from "path";
|
||||||
|
import {fileURLToPath} from "url";
|
||||||
|
import {FlatCompat} from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
...compat.config({
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
7
frontend/next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
7079
frontend/package-lock.json
generated
Normal file
40
frontend/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aos": "^2.3.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.6.5",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"next": "15.1.7",
|
||||||
|
"nodemailer": "^6.10.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"react-simple-typewriter": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.12.0",
|
||||||
|
"@tailwindcss/postcss": "^4.0.17",
|
||||||
|
"@types/aos": "^3.0.7",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.1.7",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
BIN
frontend/public/images/About_Picture.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
frontend/public/images/contact.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
frontend/public/images/dart_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/images/flutter_logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
frontend/public/images/hero.png
Normal file
|
After Width: | Height: | Size: 1003 KiB |
BIN
frontend/public/images/home_hero.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
frontend/public/images/java_logo.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
frontend/public/images/nextjs_logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/images/software_dev.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
10
frontend/public/images/svg/bootstrap.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M0,222.991225 C0,241.223474 14.7785318,256 33.0087747,256 L222.991225,256 C241.223474,256 256,241.221468 256,222.991225 L256,33.0087747 C256,14.7765263 241.221468,0 222.991225,0 L33.0087747,0 C14.7765263,0 0,14.7785318 0,33.0087747 L0,222.991225 Z" fill="#563D7C">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M106.157563,113.238095 L106.157563,76.9845938 L138.069328,76.9845938 C141.108559,76.9845938 144.039202,77.2378593 146.861345,77.7443978 C149.683488,78.2509362 152.179961,79.1554557 154.35084,80.4579832 C156.52172,81.7605107 158.258397,83.5695496 159.560924,85.8851541 C160.863452,88.2007585 161.514706,91.1675823 161.514706,94.7857143 C161.514706,101.298352 159.560944,106.001853 155.653361,108.896359 C151.745779,111.790864 146.752832,113.238095 140.67437,113.238095 L106.157563,113.238095 L106.157563,113.238095 Z M72.07493,50.5 L72.07493,205.5 L147.186975,205.5 C154.133788,205.5 160.899594,204.631661 167.484594,202.894958 C174.069594,201.158255 179.93088,198.480877 185.068627,194.862745 C190.206375,191.244613 194.294803,186.577293 197.334034,180.860644 C200.373264,175.143996 201.892857,168.37819 201.892857,160.563025 C201.892857,150.866431 199.541107,142.581033 194.837535,135.706583 C190.133963,128.832132 183.00635,124.020088 173.454482,121.270308 C180.401295,117.941627 185.647508,113.672295 189.193277,108.462185 C192.739047,103.252075 194.511905,96.7395349 194.511905,88.9243697 C194.511905,81.6881057 193.317939,75.6097352 190.929972,70.6890756 C188.542005,65.7684161 185.177193,61.8247114 180.835434,58.8578431 C176.493676,55.8909749 171.283644,53.756309 165.205182,52.4537815 C159.12672,51.151254 152.397096,50.5 145.016106,50.5 L72.07493,50.5 L72.07493,50.5 Z M106.157563,179.015406 L106.157563,136.466387 L143.279412,136.466387 C150.660401,136.466387 156.594049,138.166883 161.080532,141.567927 C165.567016,144.968971 167.810224,150.649353 167.810224,158.609244 C167.810224,162.661552 167.122789,165.990183 165.747899,168.595238 C164.373009,171.200293 162.527789,173.262597 160.212185,174.782213 C157.89658,176.301828 155.219203,177.387252 152.179972,178.038515 C149.140741,178.689779 145.956833,179.015406 142.628151,179.015406 L106.157563,179.015406 L106.157563,179.015406 Z" fill="#FFFFFF">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
6
frontend/public/images/svg/css.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 28L4 3H28L26 28L16 31L6 28Z" fill="#1172B8"/>
|
||||||
|
<path d="M26 5H16V29.5L24 27L26 5Z" fill="#33AADD"/>
|
||||||
|
<path d="M19.5 17.5H9.5L9 14L17 11.5H9L8.5 8.5H24L23.5 12L17 14.5H23L22 24L16 26L10 24L9.5 19H12.5L13 21.5L16 22.5L19 21.5L19.5 17.5Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 488 B |
18
frontend/public/images/svg/dart.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M70.534,69.696 L53.988,53.15 L54.058,172.75 L54.256,178.34 C54.338,180.97 54.826,183.938 55.64,187.014 L186.744,233.244 L219.516,218.724 L219.528,218.684 L70.534,69.696" fill="#00D2B8">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M55.64,187.014 L55.648,187.022 C55.64,186.968 55.612,186.908 55.612,186.852 C55.612,186.908 55.62,186.96 55.64,187.014 L55.64,187.014 Z M219.516,218.724 L186.744,233.244 L55.648,187.022 C58.152,196.63 63.696,207.43 69.662,213.336 L112.446,255.876 L207.576,256 L219.528,218.684 L219.516,218.724 L219.516,218.724 Z" fill="#55DDCA">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M3.034,130.116 C-1.202,134.638 0.902,143.966 7.722,150.838 L37.14,180.5 L55.64,187.014 C54.826,183.938 54.338,180.97 54.256,178.34 L54.058,172.75 L53.988,53.15 L3.034,130.116 Z" fill="#0081C6">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M187.82,54.686 C184.744,53.9 181.794,53.414 179.12,53.33 L173.212,53.126 L53.988,53.142 L219.544,218.684 L219.558,218.684 L234.098,185.88 L187.82,54.686" fill="#0079B3">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M187.67,54.654 C187.734,54.668 187.784,54.686 187.826,54.692 L187.82,54.686 C187.784,54.668 187.734,54.668 187.67,54.654 L187.67,54.654 Z M214.118,68.732 C208.11,62.674 197.452,57.168 187.826,54.692 L234.098,185.88 L219.558,218.684 L219.544,218.684 L255.076,207.336 L255.152,109.92 L214.118,68.732 L214.118,68.732 Z" fill="#00A4E4">
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
4
frontend/public/images/svg/debian.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
2
frontend/public/images/svg/docker.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#2396ED" d="M12.342 4.536l.15-.227.262.159.116.083c.28.216.869.768.996 1.684.223-.04.448-.06.673-.06.534 0 .893.124 1.097.227l.105.057.068.045.191.156-.066.2a2.044 2.044 0 01-.47.73c-.29.299-.8.652-1.609.698l-.178.005h-.148c-.37.977-.867 2.078-1.702 3.066a7.081 7.081 0 01-1.74 1.488 7.941 7.941 0 01-2.549.968c-.644.125-1.298.187-1.953.185-1.45 0-2.73-.288-3.517-.792-.703-.449-1.243-1.182-1.606-2.177a8.25 8.25 0 01-.461-2.83.516.516 0 01.432-.516l.068-.005h10.54l.092-.007.149-.016c.256-.034.646-.11.92-.27-.328-.543-.421-1.178-.268-1.854a3.3 3.3 0 01.3-.81l.108-.187zM2.89 5.784l.04.007a.127.127 0 01.077.082l.006.04v1.315l-.006.041a.127.127 0 01-.078.082l-.039.006H1.478a.124.124 0 01-.117-.088l-.007-.04V5.912l.007-.04a.127.127 0 01.078-.083l.039-.006H2.89zm1.947 0l.039.007a.127.127 0 01.078.082l.006.04v1.315l-.007.041a.127.127 0 01-.078.082l-.039.006H3.424a.125.125 0 01-.117-.088L3.3 7.23V5.913a.13.13 0 01.085-.123l.039-.007h1.413zm1.976 0l.039.007a.127.127 0 01.077.082l.007.04v1.315l-.007.041a.127.127 0 01-.078.082l-.039.006H5.4a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.039-.006h1.413zm1.952 0l.039.007a.127.127 0 01.078.082l.007.04v1.315a.13.13 0 01-.085.123l-.04.006H7.353a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.04-.006h1.412zm1.97 0l.039.007a.127.127 0 01.078.082l.006.04v1.315a.13.13 0 01-.085.123l-.039.006H9.322a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.04-.006h1.411zM4.835 3.892l.04.007a.127.127 0 01.077.081l.007.041v1.315a.13.13 0 01-.085.123l-.039.007H3.424a.125.125 0 01-.117-.09l-.007-.04V4.021a.13.13 0 01.085-.122l.039-.007h1.412zm1.976 0l.04.007a.127.127 0 01.077.081l.007.041v1.315a.13.13 0 01-.085.123l-.039.007H5.4a.125.125 0 01-.117-.09l-.006-.04V4.021l.006-.04a.127.127 0 01.078-.082l.039-.007h1.412zm1.953 0c.054 0 .1.037.117.088l.007.041v1.315a.13.13 0 01-.085.123l-.04.007H7.353a.125.125 0 01-.117-.09l-.006-.04V4.021l.006-.04a.127.127 0 01.078-.082l.04-.007h1.412zm0-1.892c.054 0 .1.037.117.088l.007.04v1.316a.13.13 0 01-.085.123l-.04.006H7.353a.124.124 0 01-.117-.088l-.006-.04V2.128l.006-.04a.127.127 0 01.078-.082L7.353 2h1.412z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
24
frontend/public/images/svg/flutter.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="-30.5 0 317 317" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="3.9517088%" y1="26.9930287%" x2="75.8970734%" y2="52.9192657%" id="linearGradient-1">
|
||||||
|
<stop stop-color="#000000" offset="0%">
|
||||||
|
|
||||||
|
</stop>
|
||||||
|
<stop stop-color="#000000" stop-opacity="0" offset="100%">
|
||||||
|
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#47C5FB" points="157.665785 0.000549356223 0.000549356223 157.665785 48.8009614 206.466197 255.267708 0.000549356223">
|
||||||
|
|
||||||
|
</polygon>
|
||||||
|
<polygon fill="#47C5FB" points="156.567183 145.396793 72.1487107 229.815265 121.132608 279.530905 169.842925 230.820587 255.267818 145.396793">
|
||||||
|
|
||||||
|
</polygon>
|
||||||
|
<polygon fill="#00569E" points="121.133047 279.531124 158.214592 316.61267 255.267159 316.61267 169.842266 230.820807">
|
||||||
|
|
||||||
|
</polygon>
|
||||||
|
<polygon fill="#00B5F8" points="71.5995742 230.364072 120.401085 181.562561 169.842046 230.821136 121.132827 279.531454">
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
20
frontend/public/images/svg/gitlab-ci.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -10 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M128.07485,236.074667 L128.07485,236.074667 L175.17885,91.1043048 L80.9708495,91.1043048 L128.07485,236.074667 L128.07485,236.074667 Z" fill="#E24329">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M128.07485,236.074423 L80.9708495,91.104061 L14.9557638,91.104061 L128.07485,236.074423 L128.07485,236.074423 Z" fill="#FC6D26">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M14.9558857,91.1044267 L14.9558857,91.1044267 L0.641828571,135.159589 C-0.663771429,139.17757 0.766171429,143.57955 4.18438095,146.06275 L128.074971,236.074789 L14.9558857,91.1044267 L14.9558857,91.1044267 Z" fill="#FCA326">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M14.9558857,91.1045486 L80.9709714,91.1045486 L52.6000762,3.79026286 C51.1408762,-0.703146667 44.7847619,-0.701927619 43.3255619,3.79026286 L14.9558857,91.1045486 L14.9558857,91.1045486 Z" fill="#E24329">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M128.07485,236.074423 L175.17885,91.104061 L241.193935,91.104061 L128.07485,236.074423 L128.07485,236.074423 Z" fill="#FC6D26">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M241.193935,91.1044267 L241.193935,91.1044267 L255.507992,135.159589 C256.813592,139.17757 255.38365,143.57955 251.96544,146.06275 L128.07485,236.074789 L241.193935,91.1044267 L241.193935,91.1044267 Z" fill="#FCA326">
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
2
frontend/public/images/svg/gitlab.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#FC6D26" d="M14.975 8.904L14.19 6.55l-1.552-4.67a.268.268 0 00-.255-.18.268.268 0 00-.254.18l-1.552 4.667H5.422L3.87 1.879a.267.267 0 00-.254-.179.267.267 0 00-.254.18l-1.55 4.667-.784 2.357a.515.515 0 00.193.583l6.78 4.812 6.778-4.812a.516.516 0 00.196-.583z"/><path fill="#E24329" d="M8 14.296l2.578-7.75H5.423L8 14.296z"/><path fill="#FC6D26" d="M8 14.296l-2.579-7.75H1.813L8 14.296z"/><path fill="#FCA326" d="M1.81 6.549l-.784 2.354a.515.515 0 00.193.583L8 14.3 1.81 6.55z"/><path fill="#E24329" d="M1.812 6.549h3.612L3.87 1.882a.268.268 0 00-.254-.18.268.268 0 00-.255.18L1.812 6.549z"/><path fill="#FC6D26" d="M8 14.296l2.578-7.75h3.614L8 14.296z"/><path fill="#FCA326" d="M14.19 6.549l.783 2.354a.514.514 0 01-.193.583L8 14.296l6.188-7.747h.001z"/><path fill="#E24329" d="M14.19 6.549H10.58l1.551-4.667a.267.267 0 01.255-.18c.115 0 .217.073.254.18l1.552 4.667z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
frontend/public/images/svg/html.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 28L4 3H28L26 28L16 31L6 28Z" fill="#E44D26"/>
|
||||||
|
<path d="M26 5H16V29.5L24 27L26 5Z" fill="#F16529"/>
|
||||||
|
<path d="M9.5 17.5L8.5 8H24L23.5 11H11.5L12 14.5H23L22 24L16 26L10 24L9.5 19H12.5L13 21.5L16 22.5L19 21.5L19.5 17.5H9.5Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 475 B |