Initial Commit

This commit is contained in:
2025-05-04 12:18:13 +02:00
commit a4f1a58f15
113 changed files with 11439 additions and 0 deletions

9
.gitignore vendored Normal file
View 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
View 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
View 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>

View File

@@ -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();
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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 {
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -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
View 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>

View File

@@ -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);
}
}

View 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
View 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>

View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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>
);
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
import Home from "@/components/Home/Home";
const HomePage = () => {
return (
<div>
<Home />
</div>
);
};
export default HomePage;

View 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>
);
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
import AboutContent from "@/components/About/AboutContent";
const AboutPage = () => {
return (
<div>
<AboutContent/>
</div>
);
};
export default AboutPage;

View 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});
}
}

View 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>
);
}

View 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
View 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;
}

View 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;

View 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>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
const LegalPage = () => {
return (
<div>
</div>
);
};
export default LegalPage;

View 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;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import RevocationComp from "@/components/Legal/RevocationComp";
const RevocationPage = () => {
return (
<div>
<RevocationComp />
</div>
);
};
export default RevocationPage;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import TermsOfUseComp from "@/components/Legal/TermsOfUseComp";
const TermsOfUsePage = () => {
return (
<div>
<TermsOfUseComp/>
</div>
);
};
export default TermsOfUsePage;

View 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>
);
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
import Services from "@/components/Services/Services";
const ContactPage = () => {
return (
<div>
<Services/>
</div>
);
};
export default ContactPage;

View 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;

View 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;

View 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;

View 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>&nbsp;
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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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))`
}}
/>
);
};

View 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;

View 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",
},
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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>
);
};

View 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',
}
];

View 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
View 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

File diff suppressed because it is too large Load Diff

40
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View 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

View 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

View 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

View 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

View 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

Some files were not shown because too many files have changed in this diff Show More