frontend and backend base setup
This commit is contained in:
parent
338adc3b6f
commit
0db2a0c647
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"cSpell.language": "en,de-DE"
|
||||||
|
}
|
@ -45,3 +45,7 @@ The project follows Domain-Driven Design (DDD) and Clean Architecture principles
|
|||||||
git clone https://inf-git.th-rosenheim.de/studavrije7683/ws24-kp-avril.git
|
git clone https://inf-git.th-rosenheim.de/studavrije7683/ws24-kp-avril.git
|
||||||
cd ws24-kp-avril
|
cd ws24-kp-avril
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- PNPM: npm install -g pnpm
|
94
_common/gRPC.proto
Normal file
94
_common/gRPC.proto
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package timetracking;
|
||||||
|
|
||||||
|
service TimeTrackingService {
|
||||||
|
rpc RegisterUser(RegisterUserRequest) returns (RegisterUserResponse);
|
||||||
|
rpc LoginUser(LoginUserRequest) returns (LoginUserResponse);
|
||||||
|
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||||
|
rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse);
|
||||||
|
rpc StartWorkSession(StartWorkSessionRequest) returns (StartWorkSessionResponse);
|
||||||
|
rpc StopWorkSession(StopWorkSessionRequest) returns (StopWorkSessionResponse);
|
||||||
|
rpc GenerateReport(GenerateReportRequest) returns (GenerateReportResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterUserRequest {
|
||||||
|
string name = 1;
|
||||||
|
string email = 2;
|
||||||
|
string password = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterUserResponse {
|
||||||
|
string userId = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginUserRequest {
|
||||||
|
string email = 1;
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginUserResponse {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserRequest {
|
||||||
|
string userId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserResponse {
|
||||||
|
string userId = 1;
|
||||||
|
string name = 2;
|
||||||
|
string email = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateProjectRequest {
|
||||||
|
string name = 1;
|
||||||
|
string clientId = 2;
|
||||||
|
string description = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateProjectResponse {
|
||||||
|
string projectId = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartWorkSessionRequest {
|
||||||
|
string userId = 1;
|
||||||
|
string projectId = 2;
|
||||||
|
string description = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartWorkSessionResponse {
|
||||||
|
string sessionId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StopWorkSessionRequest {
|
||||||
|
string sessionId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StopWorkSessionResponse {
|
||||||
|
string message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenerateReportRequest {
|
||||||
|
repeated string userIds = 1;
|
||||||
|
string startDate = 2;
|
||||||
|
string endDate = 3;
|
||||||
|
string projectId = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenerateReportResponse {
|
||||||
|
string reportId = 1;
|
||||||
|
string generatedAt = 2;
|
||||||
|
repeated WorkSession sessions = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WorkSession {
|
||||||
|
string sessionId = 1;
|
||||||
|
string userId = 2;
|
||||||
|
string startTime = 3;
|
||||||
|
string endTime = 4;
|
||||||
|
string projectId = 5;
|
||||||
|
string description = 6;
|
||||||
|
}
|
23
backend-go/cmd/actatempus/main.go
Normal file
23
backend-go/cmd/actatempus/main.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"actatempus_backend/internal/infrastructure/persistence/config"
|
||||||
|
"actatempus_backend/internal/interfaces/http"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Lese die Konfiguration ein
|
||||||
|
cfg, err := config.LoadConfig(".")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starte den HTTP-Server
|
||||||
|
server := http.NewServer(cfg)
|
||||||
|
fmt.Println("Starting ActaTempus server on port 8080...")
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
log.Fatalf("server failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
30
backend-go/go.mod
Normal file
30
backend-go/go.mod
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
module actatempus_backend
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/spf13/viper v1.19.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
62
backend-go/go.sum
Normal file
62
backend-go/go.sum
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
23
backend-go/internal/application/usecases/register_user.go
Normal file
23
backend-go/internal/application/usecases/register_user.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package usecases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"actatempus_backend/internal/domain/entities"
|
||||||
|
"actatempus_backend/internal/domain/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegisterUserUseCase struct {
|
||||||
|
userRepository repositories.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegisterUserUseCase(userRepo repositories.UserRepository) *RegisterUserUseCase {
|
||||||
|
return &RegisterUserUseCase{userRepository: userRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *RegisterUserUseCase) Execute(name, email, password string) error {
|
||||||
|
user := &entities.User{
|
||||||
|
Name: name,
|
||||||
|
Email: email,
|
||||||
|
Password: password, // In der Realität: Passwörter hashen
|
||||||
|
}
|
||||||
|
return uc.userRepository.Create(user)
|
||||||
|
}
|
8
backend-go/internal/domain/entities/user.go
Normal file
8
backend-go/internal/domain/entities/user.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import "actatempus_backend/internal/domain/entities"
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
Create(user *entities.User) error
|
||||||
|
FindByEmail(email string) (*entities.User, error)
|
||||||
|
FindByID(id string) (*entities.User, error)
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DatabaseURL string
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
viper.AddConfigPath(path)
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
DatabaseURL: viper.GetString("database.url"),
|
||||||
|
Port: viper.GetString("server.port"),
|
||||||
|
}, nil
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"actatempus_backend/internal/domain/entities"
|
||||||
|
"actatempus_backend/internal/domain/repositories"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresUserRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresUserRepository(db *sql.DB) repositories.UserRepository {
|
||||||
|
return &PostgresUserRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresUserRepository) Create(user *entities.User) error {
|
||||||
|
_, err := r.db.Exec("INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)",
|
||||||
|
user.ID, user.Name, user.Email, user.Password)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresUserRepository) FindByEmail(email string) (*entities.User, error) {
|
||||||
|
var user entities.User
|
||||||
|
err := r.db.QueryRow("SELECT id, name, email, password FROM users WHERE email = $1", email).
|
||||||
|
Scan(&user.ID, &user.Name, &user.Email, &user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresUserRepository) FindByID(id string) (*entities.User, error) {
|
||||||
|
var user entities.User
|
||||||
|
err := r.db.QueryRow("SELECT id, name, email, password FROM users WHERE id = $1", id).
|
||||||
|
Scan(&user.ID, &user.Name, &user.Email, &user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
22
backend-go/internal/interfaces/http/server.go
Normal file
22
backend-go/internal/interfaces/http/server.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"actatempus_backend/internal/infrastructure/persistence/config"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(cfg *config.Config) *Server {
|
||||||
|
return &Server{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, "Welcome to ActaTempus!")
|
||||||
|
})
|
||||||
|
return http.ListenAndServe(fmt.Sprintf(":%s", s.cfg.Port), nil)
|
||||||
|
}
|
Binary file not shown.
BIN
docs/KP_Spec-3.pdf
Normal file
BIN
docs/KP_Spec-3.pdf
Normal file
Binary file not shown.
3
frontend-react/.eslintrc.json
Normal file
3
frontend-react/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
40
frontend-react/.gitignore
vendored
Normal file
40
frontend-react/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 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*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
36
frontend-react/README.md
Normal file
36
frontend-react/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
23
frontend-react/messages/de.json
Normal file
23
frontend-react/messages/de.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Error": {
|
||||||
|
"description": "<p>Es ist leider ein Problem aufgetreten.</p><p>Du kannst versuchen <retry>diese Seite neu zu laden</retry>.</p>",
|
||||||
|
"title": "Etwas ist schief gelaufen!",
|
||||||
|
"backToHome": "Zurück zur Startseite",
|
||||||
|
"back": "Zurück",
|
||||||
|
"defaultErrorCode": "Fehler",
|
||||||
|
"defaultErrorMessage": "Es ist ein Problem aufgetreten."
|
||||||
|
|
||||||
|
},
|
||||||
|
"LocaleSwitcher": {
|
||||||
|
"label": "Sprache ändern",
|
||||||
|
"locale": "{locale, select, de {🇩🇪 Deutsch} en {🇺🇸 English} other {Unbekannt}}"
|
||||||
|
},
|
||||||
|
"Navigation": {
|
||||||
|
"home": "Start",
|
||||||
|
"pathnames": "Pfadnamen"
|
||||||
|
},
|
||||||
|
"NotFoundPage": {
|
||||||
|
"description": "Bitte überprüfe die Addressleiste deines Browsers oder verwende die Navigation um zu einer bekannten Seite zu wechseln.",
|
||||||
|
"title": "Seite nicht gefunden"
|
||||||
|
}
|
||||||
|
}
|
27
frontend-react/messages/en.json
Normal file
27
frontend-react/messages/en.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Hello world!",
|
||||||
|
"about": "Go to the about page"
|
||||||
|
},
|
||||||
|
"Error": {
|
||||||
|
"description": "<p>We've unfortunately encountered an error.</p><p>You can try to <retry>reload the page</retry> you were visiting.</p>",
|
||||||
|
"title": "Something went wrong!",
|
||||||
|
"backToHome": "Back to home",
|
||||||
|
"back": "Back",
|
||||||
|
"defaultErrorCode": "Error",
|
||||||
|
"defaultErrorMessage": "An error occurred."
|
||||||
|
},
|
||||||
|
"LocaleSwitcher": {
|
||||||
|
"label": "Change language",
|
||||||
|
"locale": "{locale, select, de {🇩🇪 Deutsch} en {🇺🇸 English} other {Unknown}}"
|
||||||
|
},
|
||||||
|
"Navigation": {
|
||||||
|
"home": "Home",
|
||||||
|
"pathnames": "Pathnames"
|
||||||
|
},
|
||||||
|
"NotFoundPage": {
|
||||||
|
"description": "Please check the address bar of your browser or use the navigation to go to a known page.",
|
||||||
|
"title": "Page not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
frontend-react/next.config.ts
Normal file
13
frontend-react/next.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
sassOptions: {
|
||||||
|
silenceDeprecations: ['legacy-js-api'],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNextIntl(nextConfig);
|
8146
frontend-react/package-lock.json
generated
Normal file
8146
frontend-react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
frontend-react/package.json
Normal file
32
frontend-react/package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend-react",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.456.0",
|
||||||
|
"next": "15.0.3",
|
||||||
|
"next-intl": "^3.25.0",
|
||||||
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
|
"react-dom": "19.0.0-rc-66855b96-20241106"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "15.0.3",
|
||||||
|
"i18next-parser": "^9.0.2",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"sass": "^1.80.6",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
8
frontend-react/postcss.config.mjs
Normal file
8
frontend-react/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
1
frontend-react/public/file.svg
Normal file
1
frontend-react/public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 391 B |
1
frontend-react/public/globe.svg
Normal file
1
frontend-react/public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend-react/public/window.svg
Normal file
1
frontend-react/public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
After Width: | Height: | Size: 385 B |
11
frontend-react/src/app/[locale]/(auth)/layout.tsx
Normal file
11
frontend-react/src/app/[locale]/(auth)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import BaseLayout from "@/components/Layout/BaseLayout";
|
||||||
|
import { routing } from "@/i18n/routing";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: Props) {
|
||||||
|
return <BaseLayout locale={routing.defaultLocale}>{children}</BaseLayout>;
|
||||||
|
}
|
9
frontend-react/src/app/[locale]/(auth)/login/layout.tsx
Normal file
9
frontend-react/src/app/[locale]/(auth)/login/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const LoginLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginLayout;
|
65
frontend-react/src/app/[locale]/(auth)/login/page.tsx
Normal file
65
frontend-react/src/app/[locale]/(auth)/login/page.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log("Email:", email);
|
||||||
|
console.log("Password:", password);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-gray-700 font-medium mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-gray-700 font-medium mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition duration-200">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<Link className="block text-center mt-4 text-blue-500" href="register">
|
||||||
|
Create an account
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
16
frontend-react/src/app/[locale]/(landing)/layout.tsx
Normal file
16
frontend-react/src/app/[locale]/(landing)/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import BaseLayout from "@/components/Layout/BaseLayout";
|
||||||
|
import { routing } from "@/i18n/routing";
|
||||||
|
import UnauthenticatedLayout from "@/components/Layout/UnauthenticatedLayout";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<BaseLayout locale={routing.defaultLocale}>
|
||||||
|
<UnauthenticatedLayout>{children}</UnauthenticatedLayout>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
29
frontend-react/src/app/[locale]/(landing)/page.tsx
Normal file
29
frontend-react/src/app/[locale]/(landing)/page.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex items-center justify-center">
|
||||||
|
<div className="max-w-2xl pt-5 sm:pt-10 lg:pt-20">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-balance text-5xl font-semibold tracking-tight text-gray-900 sm:text-7xl">
|
||||||
|
Track your working hours with ease
|
||||||
|
</h1>
|
||||||
|
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
|
||||||
|
With ActaTempus, you can easily track your working hours and get
|
||||||
|
insights into your work habits. Get started today and be more
|
||||||
|
productive.
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||||
|
Register
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-sm/6 font-semibold text-gray-900">
|
||||||
|
Learn more <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
11
frontend-react/src/app/layout.tsx
Normal file
11
frontend-react/src/app/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {ReactNode} from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Since we have a `not-found.tsx` page on the root, a layout file
|
||||||
|
// is required, even if it's just passing children through.
|
||||||
|
export default function RootLayout({children}: Props) {
|
||||||
|
return children;
|
||||||
|
}
|
15
frontend-react/src/app/not-found.tsx
Normal file
15
frontend-react/src/app/not-found.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import BaseLayout from '@/components/Layout/BaseLayout';
|
||||||
|
import Error404Page from '@/components/ErrorPages/404';
|
||||||
|
import {routing} from '@/i18n/routing';
|
||||||
|
|
||||||
|
// This page renders when a route like `/unknown.txt` is requested.
|
||||||
|
// In this case, the layout at `app/[locale]/layout.tsx` receives
|
||||||
|
// an invalid value as the `[locale]` param and calls `notFound()`.
|
||||||
|
|
||||||
|
export default function GlobalNotFound() {
|
||||||
|
return (
|
||||||
|
<BaseLayout locale={routing.defaultLocale}>
|
||||||
|
<Error404Page />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
6
frontend-react/src/app/page.tsx
Normal file
6
frontend-react/src/app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {redirect} from 'next/navigation';
|
||||||
|
|
||||||
|
// This page only renders when the app is built statically (output: 'export')
|
||||||
|
export default function RootPage() {
|
||||||
|
redirect('/en');
|
||||||
|
}
|
11
frontend-react/src/components/ErrorPages/404.tsx
Normal file
11
frontend-react/src/components/ErrorPages/404.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import ErrorPageLayout from './ErrorPageLayout';
|
||||||
|
|
||||||
|
export default function Error404Page() {
|
||||||
|
const t = useTranslations('NotFoundPage');
|
||||||
|
return (
|
||||||
|
<ErrorPageLayout errorCode={404} errorMessage={t('title')}>
|
||||||
|
<p className="text-center mt-4">{t('description')}</p>
|
||||||
|
</ErrorPageLayout>
|
||||||
|
);
|
||||||
|
}
|
15
frontend-react/src/components/ErrorPages/BackButton.tsx
Normal file
15
frontend-react/src/components/ErrorPages/BackButton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"use client"; // Kennzeichnet die Komponente als Client-Komponente
|
||||||
|
|
||||||
|
type BackButtonProps = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BackButton({ label }: BackButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => window.history.back()}
|
||||||
|
className="inline-flex px-6 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg shadow-md">
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
41
frontend-react/src/components/ErrorPages/ErrorPageLayout.tsx
Normal file
41
frontend-react/src/components/ErrorPages/ErrorPageLayout.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ThemeProvider } from "@/context/ThemeContext";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import BackButton from "./BackButton";
|
||||||
|
import HomeButton from "./HomeButton";
|
||||||
|
|
||||||
|
type ErrorPageLayoutProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
errorCode?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ErrorPageLayout({
|
||||||
|
children,
|
||||||
|
errorCode,
|
||||||
|
errorMessage,
|
||||||
|
}: ErrorPageLayoutProps) {
|
||||||
|
const t = useTranslations("Error");
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-100 dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||||
|
<div className="flex flex-col items-center p-8">
|
||||||
|
<AlertCircle className="h-16 w-16 text-red-600 mb-4" />
|
||||||
|
<h1 className="text-5xl font-bold mb-4">
|
||||||
|
{errorCode || t("defaultErrorCode")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl mb-6">
|
||||||
|
{errorMessage || t("defaultErrorMessage")}
|
||||||
|
</p>
|
||||||
|
<div className="row space-x-4">
|
||||||
|
<HomeButton label={t("backToHome")} />
|
||||||
|
|
||||||
|
<BackButton label={t("back")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
15
frontend-react/src/components/ErrorPages/HomeButton.tsx
Normal file
15
frontend-react/src/components/ErrorPages/HomeButton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type HomeButtonProps = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HomeButton({ label }: HomeButtonProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex px-6 py-3 bg-cyan-500 hover:bg-cyan-600 text-white hover:text-white font-semibold rounded-lg shadow-md">
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
29
frontend-react/src/components/Layout/AuthenticatedLayout.tsx
Normal file
29
frontend-react/src/components/Layout/AuthenticatedLayout.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Header from "./Header/Header";
|
||||||
|
|
||||||
|
type AuthenticatedLayoutProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuthenticatedLayout({
|
||||||
|
children,
|
||||||
|
}: AuthenticatedLayoutProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Beispiel zur Überprüfung der Authentifizierung; ersetze dies mit deiner Authentifizierungslogik
|
||||||
|
const isAuthenticated = true; // Hier eine echte Überprüfung einfügen
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col bg-slate-100 dark:bg-slate-900">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-grow p-4">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
44
frontend-react/src/components/Layout/BaseLayout.tsx
Normal file
44
frontend-react/src/components/Layout/BaseLayout.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ThemeProvider } from "@/context/ThemeContext";
|
||||||
|
import "@/styles/globals.scss";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
import { getMessages } from "next-intl/server";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
const geistSans = localFont({
|
||||||
|
src: "./fonts/GeistVF.woff",
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
const geistMono = localFont({
|
||||||
|
src: "./fonts/GeistMonoVF.woff",
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Time Tracker",
|
||||||
|
description: "Time Tracker",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BaseLayout({ children, locale }: Props) {
|
||||||
|
const messages = await getMessages();
|
||||||
|
return (
|
||||||
|
<html lang={locale}>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased `}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</ThemeProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
24
frontend-react/src/components/Layout/Header/Header.tsx
Normal file
24
frontend-react/src/components/Layout/Header/Header.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import LocaleSwitcher from "../Navigation/LocaleSwitcher";
|
||||||
|
import NavigationBar from "../Navigation/NavigationBar";
|
||||||
|
import { ThemeSwitcher } from "../Navigation/ThemeSwitcher";
|
||||||
|
|
||||||
|
const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-900 shadow-md">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
ActaTempus
|
||||||
|
</div>
|
||||||
|
<NavigationBar />
|
||||||
|
{/* Theme Switcher Dropdown */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
<LocaleSwitcher />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
import {useLocale, useTranslations} from 'next-intl';
|
||||||
|
import {routing} from '@/i18n/routing';
|
||||||
|
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
|
||||||
|
|
||||||
|
export default function LocaleSwitcher() {
|
||||||
|
const t = useTranslations('LocaleSwitcher');
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocaleSwitcherSelect defaultValue={locale} label={t('label')}>
|
||||||
|
{routing.locales.map((cur) => (
|
||||||
|
<option key={cur} value={cur}>
|
||||||
|
{t('locale', {locale: cur})}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</LocaleSwitcherSelect>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Locale, usePathname, useRouter } from "@/i18n/routing";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { ChangeEvent, ReactNode, useTransition } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
defaultValue: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LocaleSwitcherSelect({
|
||||||
|
children,
|
||||||
|
defaultValue,
|
||||||
|
label,
|
||||||
|
}: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||||
|
const nextLocale = event.target.value as Locale;
|
||||||
|
startTransition(() => {
|
||||||
|
router.replace(
|
||||||
|
// @ts-expect-error -- TypeScript will validate that only known `params`
|
||||||
|
// are used in combination with a given `pathname`. Since the two will
|
||||||
|
// always match for the current route, we can skip runtime checks.
|
||||||
|
{ pathname, params },
|
||||||
|
{ locale: nextLocale }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="relative text-gray-400">
|
||||||
|
<p className="sr-only">{label}</p>
|
||||||
|
<select
|
||||||
|
className="inline-flex appearance-none bg-transparent py-3 pl-2 pr-6"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
disabled={isPending}
|
||||||
|
onChange={onSelectChange}>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function NavigationBar() {
|
||||||
|
const t = useTranslations("Navigation");
|
||||||
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Menu Button for Mobile */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<button onClick={() => setMenuOpen(!menuOpen)}>
|
||||||
|
{menuOpen ? (
|
||||||
|
<X className="h-6 w-6 text-gray-900 dark:text-white" />
|
||||||
|
) : (
|
||||||
|
<Menu className="h-6 w-6 text-gray-900 dark:text-white" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<nav
|
||||||
|
className={`md:flex ${
|
||||||
|
menuOpen ? "block" : "hidden"
|
||||||
|
} md:block space-x-4`}>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||||
|
{t("home")}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/contact"
|
||||||
|
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useSelectedLayoutSegment} from 'next/navigation';
|
||||||
|
import {ComponentProps} from 'react';
|
||||||
|
import {Link} from '@/i18n/routing';
|
||||||
|
|
||||||
|
export default function NavigationLink({
|
||||||
|
href,
|
||||||
|
...rest
|
||||||
|
}: ComponentProps<typeof Link>) {
|
||||||
|
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||||
|
const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/';
|
||||||
|
const isActive = pathname === href;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={clsx(
|
||||||
|
'inline-block px-2 py-3 transition-colors',
|
||||||
|
isActive ? 'text-white' : 'text-gray-400 hover:text-gray-200'
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
|
import { Monitor, Sun, Moon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ThemeSwitcher() {
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme: string) => {
|
||||||
|
setTheme(newTheme);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
|
className="flex items-center space-x-2 p-2 rounded hover:bg-gray-300 dark:hover:bg-gray-600">
|
||||||
|
<Monitor className="h-5 w-5 text-gray-800 dark:text-gray-200" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded shadow-lg z-10">
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
onClick={() => handleThemeChange("light")}
|
||||||
|
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||||
|
<Sun className="inline-block h-5 w-5 mr-2 text-yellow-500" />{" "}
|
||||||
|
Light
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
onClick={() => handleThemeChange("dark")}
|
||||||
|
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||||
|
<Moon className="inline-block h-5 w-5 mr-2 text-blue-500" /> Dark
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
onClick={() => handleThemeChange("system")}
|
||||||
|
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||||
|
<Monitor className="inline-block h-5 w-5 mr-2 text-gray-800 dark:text-gray-200" />{" "}
|
||||||
|
Default
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import Header from './Header/Header';
|
||||||
|
|
||||||
|
type UnauthenticatedLayoutProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col bg-white dark:bg-gray-800">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-grow p-4">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
BIN
frontend-react/src/components/Layout/fonts/GeistMonoVF.woff
Normal file
BIN
frontend-react/src/components/Layout/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
frontend-react/src/components/Layout/fonts/GeistVF.woff
Normal file
BIN
frontend-react/src/components/Layout/fonts/GeistVF.woff
Normal file
Binary file not shown.
56
frontend-react/src/context/ThemeContext.tsx
Normal file
56
frontend-react/src/context/ThemeContext.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type ThemeContextType = {
|
||||||
|
theme: string;
|
||||||
|
setTheme: (theme: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState<string>('system');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'system';
|
||||||
|
setTheme(savedTheme);
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyTheme = (selectedTheme: string) => {
|
||||||
|
if (selectedTheme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else if (selectedTheme === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (systemPrefersDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme: string) => {
|
||||||
|
setTheme(newTheme);
|
||||||
|
applyTheme(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme: handleThemeChange }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
18
frontend-react/src/i18n/request.ts
Normal file
18
frontend-react/src/i18n/request.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {getRequestConfig} from 'next-intl/server';
|
||||||
|
import {routing} from './routing';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({requestLocale}) => {
|
||||||
|
// This typically corresponds to the `[locale]` segment
|
||||||
|
let locale = await requestLocale;
|
||||||
|
type LocaleType = (typeof routing.locales)[number];
|
||||||
|
|
||||||
|
// Ensure that a valid locale is used
|
||||||
|
if (!locale || !routing.locales.includes(locale as LocaleType)) {
|
||||||
|
locale = routing.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../../messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
20
frontend-react/src/i18n/routing.ts
Normal file
20
frontend-react/src/i18n/routing.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {createNavigation} from 'next-intl/navigation';
|
||||||
|
import {defineRouting} from 'next-intl/routing';
|
||||||
|
|
||||||
|
export const routing = defineRouting({
|
||||||
|
locales: ['en', 'de'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
pathnames: {
|
||||||
|
'/': '/',
|
||||||
|
'/pathnames': {
|
||||||
|
en: '/pathnames',
|
||||||
|
de: '/pfadnamen'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Pathnames = keyof typeof routing.pathnames;
|
||||||
|
export type Locale = (typeof routing.locales)[number];
|
||||||
|
|
||||||
|
export const {Link, getPathname, redirect, usePathname, useRouter} =
|
||||||
|
createNavigation(routing);
|
20
frontend-react/src/middleware.ts
Normal file
20
frontend-react/src/middleware.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import createMiddleware from 'next-intl/middleware';
|
||||||
|
import {routing} from './i18n/routing';
|
||||||
|
|
||||||
|
export default createMiddleware(routing);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// Match only internationalized pathnames
|
||||||
|
matcher: [
|
||||||
|
// Enable a redirect to a matching locale at the root
|
||||||
|
'/',
|
||||||
|
|
||||||
|
// Set a cookie to remember the previous locale for
|
||||||
|
// all requests that have a locale prefix
|
||||||
|
'/(de|en)/:path*',
|
||||||
|
|
||||||
|
// Enable redirects that add missing locales
|
||||||
|
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||||
|
'/((?!_next|_vercel|.*\\..*).*)'
|
||||||
|
]
|
||||||
|
};
|
42
frontend-react/src/styles/globals.scss
Normal file
42
frontend-react/src/styles/globals.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
--primary-color: #1d4ed8; /* Primärfarbe für Akzente */
|
||||||
|
--secondary-color: #3b82f6; /* Sekundärfarbe für Hover-Effekte */
|
||||||
|
--border-color: #d1d5db; /* Farbe für Ränder */
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1); /* Schattenfarbe */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
--primary-color: #2563eb; /* Angepasste Primärfarbe für den Dunkelmodus */
|
||||||
|
--secondary-color: #60a5fa; /* Angepasste Sekundärfarbe */
|
||||||
|
--border-color: #374151; /* Angepasste Farbe für Ränder */
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.4); /* Stärkere Schattenfarbe */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
//color: var(--foreground);
|
||||||
|
//background: var(--background);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.6; /* Verbesserte Lesbarkeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
}
|
23
frontend-react/tailwind.config.ts
Normal file
23
frontend-react/tailwind.config.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
darkMode: "selector",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
//background: "var(--background)",
|
||||||
|
//foreground: "var(--foreground)",
|
||||||
|
//primary: "var(--primary-color)",
|
||||||
|
//secondary: "var(--secondary-color)",
|
||||||
|
//border: "var(--border-color)",
|
||||||
|
//shadow: "var(--shadow-color)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
27
frontend-react/tsconfig.json
Normal file
27
frontend-react/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user