1 Commits

Author SHA1 Message Date
Tuan-Dat Tran
68d7cbd32e docker cicd
Some checks reported warnings
Build Docker Image / Build (push) Has been cancelled
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2024-08-31 21:56:25 +02:00
36 changed files with 2096 additions and 4837 deletions

View File

@@ -1,6 +0,0 @@
**/target
**/dist
LICENSES
LICENSE
temp
README.md

View File

@@ -0,0 +1,28 @@
name: Build Docker Image
on:
push:
branches:
- main
- dev
- cicd
jobs:
build:
name: Build
runs-on: [ubuntu-latest, aya01]
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: user/app:latest

4856
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,24 @@
[package]
name = "athome"
version = "0.4.1"
version = "0.2.2"
authors = ["Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }
dioxus = { version = "0.6", features = ["fullstack", "router"] }
serde = { version = "1.0.197", features = ["derive"] }
dioxus = { version = "0.5", features = ["fullstack", "router"] }
# Debug
tracing = "0.1.40"
dioxus-logger = "0.6.0"
dioxus-i18n = "0.4.0"
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
dioxus-logger = "0.5.0"
manganis = "0.2.2"
dioxus-free-icons = { version = "0.8", features = ["font-awesome-brands"] }
dioxus-sdk = { version = "0.5.0", features = ["i18n"] }
lazy_static = "1.4.0"
[features]
default = []
server = ["dioxus/axum"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
server = ["dioxus/server"]
[profile]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"

View File

@@ -24,4 +24,20 @@ title = "Tuan-Dat Tran"
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "assets", "languages"]
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = ["tailwind.css"]
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View File

@@ -1,36 +1,20 @@
FROM rust:1 AS chef
RUN cargo install cargo-chef
WORKDIR /app
FROM rust:1.80.1 AS dioxus
RUN cargo install dioxus-cli@^0.5
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM dioxus AS builder
WORKDIR /athome/
RUN apt-get update && apt-get install nodejs npm libssl-dev musl-tools -y && rm -rf /var/lib/apt/lists/*
RUN npm install -D tailwindcss
COPY ./src/ ./src/
COPY ./assets/ ./assets/
COPY ./Cargo.toml ./Cargo.toml
COPY ./input.css ./input.css
COPY ./Dioxus.toml ./Dioxus.toml
COPY ./tailwind.config.js ./tailwind.config.js
RUN npx tailwindcss -i ./input.css -o ./assets/tailwind.css
RUN dx build --platform fullstack --release
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
# Install `dx`
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
RUN cargo binstall dioxus-cli --root /.cargo -y --force
ENV PATH="/.cargo/bin:$PATH"
# Create the final bundle folder. Bundle always executes in release mode with optimizations enabled
RUN dx bundle --platform web
FROM debian:bookworm-slim AS runtime
# Install ca-certificates for HTTPS requests if the server makes any outgoing calls
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/dx/athome/release/web/ /usr/local/app
# set our port and make sure to listen for all connections
ENV PORT=8080
ENV IP=0.0.0.0
# expose the port 8080
EXPOSE 8080
WORKDIR /usr/local/app
ENTRYPOINT [ "/usr/local/app/server" ]
FROM dioxus AS runner
WORKDIR /app/
COPY --from=builder /athome/docs/ ./docs/
CMD [ "./docs/athome" ]

View File

@@ -2,16 +2,6 @@
My personal website.
## Usage
```sh
npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch
```
```sh
dx serve --platform web
```
## Screenshot
[[./resources/screenshot.webp]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
assets/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

BIN
assets/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +0,0 @@
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"></path> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"></path> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"></path> </g> </g> </g> </svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,5 +0,0 @@
<svg id="Group_1282" data-name="Group 1282" xmlns="http://www.w3.org/2000/svg" width="76.624" height="65.326" viewBox="0 0 76.624 65.326">
<path id="Path_2525" data-name="Path 2525" d="M1165,274.515a1.2,1.2,0,0,0,1.21-1.269c0-.9-.543-1.33-1.657-1.33h-1.8v4.712h.677v-2.054h.832l.019.025,1.291,2.029h.724l-1.389-2.1Zm-.783-.472h-.785V272.45h.995c.514,0,1.1.084,1.1.757,0,.774-.593.836-1.314.836" transform="translate(-1092.136 -213.406)" fill="#0a66c2"/>
<path id="Path_2520" data-name="Path 2520" d="M958.98,112.559h-9.6V97.525c0-3.585-.064-8.2-4.993-8.2-5,0-5.765,3.906-5.765,7.939v15.294h-9.6V81.642h9.216v4.225h.129a10.1,10.1,0,0,1,9.093-4.994c9.73,0,11.524,6.4,11.524,14.726ZM918.19,77.416a5.571,5.571,0,1,1,5.57-5.572,5.571,5.571,0,0,1-5.57,5.572m4.8,35.143h-9.61V81.642h9.61Zm40.776-55.2h-55.21a4.728,4.728,0,0,0-4.781,4.67v55.439a4.731,4.731,0,0,0,4.781,4.675h55.21a4.741,4.741,0,0,0,4.8-4.675V62.025a4.738,4.738,0,0,0-4.8-4.67" transform="translate(-903.776 -57.355)" fill="#0a66c2"/>
<path id="Path_2526" data-name="Path 2526" d="M1156.525,264.22a4.418,4.418,0,1,0,.085,0h-.085m0,8.33a3.874,3.874,0,1,1,3.809-3.938c0,.022,0,.043,0,.065a3.791,3.791,0,0,1-3.708,3.871h-.1" transform="translate(-1084.362 -207.809)" fill="#0a66c2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -2,19 +2,25 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.tudattr.dev/</loc>
<lastmod>2025-01-12</lastmod>
<lastmod>2024-07-25</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.tudattr.dev/resume</loc>
<lastmod>2025-01-12</lastmod>
<lastmod>2024-07-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.tudattr.dev/publications</loc>
<lastmod>2025-01-12</lastmod>
<lastmod>2024-07-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.tudattr.dev/consulting</loc>
<lastmod>2024-07-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>

View File

@@ -588,6 +588,10 @@ video {
}
}
.static {
position: static;
}
.absolute {
position: absolute;
}
@@ -604,11 +608,21 @@ video {
inset-inline-start: -0.375rem;
}
.mx-16 {
margin-left: 4rem;
margin-right: 4rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
@@ -662,6 +676,15 @@ video {
display: flex;
}
.size-auto {
width: auto;
height: auto;
}
.h-16 {
height: 4rem;
}
.h-24 {
height: 6rem;
}
@@ -682,10 +705,18 @@ video {
height: 1px;
}
.h-screen {
height: 100vh;
}
.min-h-screen {
min-height: 100vh;
}
.w-16 {
width: 4rem;
}
.w-24 {
width: 6rem;
}
@@ -723,18 +754,10 @@ video {
max-width: 36rem;
}
.flex-auto {
flex: 1 1 auto;
}
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
}
.cursor-pointer {
cursor: pointer;
}
@@ -747,14 +770,6 @@ video {
flex-wrap: wrap;
}
.content-center {
align-content: center;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
@@ -771,22 +786,14 @@ video {
justify-content: space-between;
}
.justify-stretch {
justify-content: stretch;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
.space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
@@ -795,14 +802,8 @@ video {
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(2rem * var(--tw-space-y-reverse));
}
.self-stretch {
align-self: stretch;
.overflow-x-auto {
overflow-x: auto;
}
.rounded {
@@ -847,69 +848,69 @@ video {
border-inline-start-width: 1px;
}
.border-gray-600 {
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.border-gray-700 {
.border-white {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity));
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.border-gray-900 {
--tw-border-opacity: 1;
border-color: rgb(17 24 39 / var(--tw-border-opacity));
}
.bg-blue-900 {
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.bg-gray-700 {
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-gray-800 {
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.bg-gray-900 {
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-green-900 {
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
}
.bg-indigo-900 {
.bg-indigo-100 {
--tw-bg-opacity: 1;
background-color: rgb(49 46 129 / var(--tw-bg-opacity));
background-color: rgb(224 231 255 / var(--tw-bg-opacity));
}
.bg-pink-900 {
.bg-pink-100 {
--tw-bg-opacity: 1;
background-color: rgb(131 24 67 / var(--tw-bg-opacity));
background-color: rgb(252 231 243 / var(--tw-bg-opacity));
}
.bg-purple-900 {
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(88 28 135 / var(--tw-bg-opacity));
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
}
.bg-red-900 {
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
}
.bg-yellow-900 {
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
}
.bg-gradient-to-br {
@@ -955,6 +956,10 @@ video {
background-clip: text;
}
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
@@ -1087,24 +1092,14 @@ video {
letter-spacing: 0.1em;
}
.text-blue-300 {
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity));
color: rgb(30 64 175 / var(--tw-text-opacity));
}
.text-blue-500 {
.text-cyan-600 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.text-cyan-400 {
--tw-text-opacity: 1;
color: rgb(34 211 238 / var(--tw-text-opacity));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
color: rgb(8 145 178 / var(--tw-text-opacity));
}
.text-gray-400 {
@@ -1117,34 +1112,44 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-gray-900 {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.text-green-300 {
.text-green-800 {
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity));
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-indigo-300 {
.text-indigo-800 {
--tw-text-opacity: 1;
color: rgb(165 180 252 / var(--tw-text-opacity));
color: rgb(55 48 163 / var(--tw-text-opacity));
}
.text-pink-300 {
.text-pink-800 {
--tw-text-opacity: 1;
color: rgb(249 168 212 / var(--tw-text-opacity));
color: rgb(157 23 77 / var(--tw-text-opacity));
}
.text-purple-300 {
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(216 180 254 / var(--tw-text-opacity));
color: rgb(107 33 168 / var(--tw-text-opacity));
}
.text-red-300 {
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(252 165 165 / var(--tw-text-opacity));
color: rgb(153 27 27 / var(--tw-text-opacity));
}
.text-transparent {
@@ -1156,9 +1161,9 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-yellow-300 {
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity));
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.shadow {
@@ -1198,17 +1203,20 @@ video {
transition-duration: 300ms;
}
@custom-variant dark (&:where(.dark, .dark *));
.hover\:bg-gray-700:hover {
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.hover\:bg-gradient-to-br:hover {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
}
.hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@@ -1227,9 +1235,9 @@ video {
z-index: 10;
}
.focus\:text-white:focus {
.focus\:text-blue-700:focus {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.focus\:outline-none:focus {
@@ -1249,14 +1257,14 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-500:focus {
.focus\:ring-blue-700:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
--tw-ring-color: rgb(29 78 216 / var(--tw-ring-opacity));
}
.focus\:ring-cyan-800:focus {
.focus\:ring-cyan-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(21 94 117 / var(--tw-ring-opacity));
--tw-ring-color: rgb(103 232 249 / var(--tw-ring-opacity));
}
.group:hover .group-hover\:from-green-400 {
@@ -1278,12 +1286,6 @@ video {
flex-direction: row;
}
.sm\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
@@ -1308,6 +1310,11 @@ video {
font-size: 3rem;
line-height: 1;
}
.md\:text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
}
@media (min-width: 1024px) {
@@ -1317,3 +1324,163 @@ video {
}
}
@media (prefers-color-scheme: dark) {
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
}
.dark\:border-gray-700 {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity));
}
.dark\:border-gray-900 {
--tw-border-opacity: 1;
border-color: rgb(17 24 39 / var(--tw-border-opacity));
}
.dark\:bg-blue-900 {
--tw-bg-opacity: 1;
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}
.dark\:bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.dark\:bg-green-900 {
--tw-bg-opacity: 1;
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
}
.dark\:bg-indigo-900 {
--tw-bg-opacity: 1;
background-color: rgb(49 46 129 / var(--tw-bg-opacity));
}
.dark\:bg-pink-900 {
--tw-bg-opacity: 1;
background-color: rgb(131 24 67 / var(--tw-bg-opacity));
}
.dark\:bg-purple-900 {
--tw-bg-opacity: 1;
background-color: rgb(88 28 135 / var(--tw-bg-opacity));
}
.dark\:bg-red-900 {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900 {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:text-blue-300 {
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity));
}
.dark\:text-cyan-400 {
--tw-text-opacity: 1;
color: rgb(34 211 238 / var(--tw-text-opacity));
}
.dark\:text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.dark\:text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.dark\:text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.dark\:text-green-300 {
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity));
}
.dark\:text-indigo-300 {
--tw-text-opacity: 1;
color: rgb(165 180 252 / var(--tw-text-opacity));
}
.dark\:text-pink-300 {
--tw-text-opacity: 1;
color: rgb(249 168 212 / var(--tw-text-opacity));
}
.dark\:text-purple-300 {
--tw-text-opacity: 1;
color: rgb(216 180 254 / var(--tw-text-opacity));
}
.dark\:text-red-300 {
--tw-text-opacity: 1;
color: rgb(252 165 165 / var(--tw-text-opacity));
}
.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-yellow-300 {
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity));
}
.dark\:hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:text-white:focus {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-cyan-800:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(21 94 117 / var(--tw-ring-opacity));
}
}
@media (min-width: 768px) {
@media (prefers-color-scheme: dark) {
.md\:dark\:text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
}
}

View File

@@ -1,6 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@custom-variant dark (&:where(.dark, .dark *));
@tailwind utilities;

View File

@@ -1,153 +0,0 @@
headers_home = Home
headers_cv = Lebenslauf
headers_publications_projects = Artikel/Projekte
headers_about = Impressum
headers_language_buttons_english = 🇬🇧 Englisch
headers_language_buttons_german = 🇩🇪 Deutsch
home_card_name = Tuan-Dat Tran
home_card_gender = {""}
home_card_text =
Hallihallo! 👋🏻👋🏼👋🏽👋🏾👋🏿
Willkommen auf meiner kleinen Webseite im World Wide Web.
Mein Name ist Tuan und ich bin Linux-Bastler, IT-Security Enthusiast und IT-Automatisierer aus Leidenschaft.
home_card_contact_button = Get in touch.
cv_introduction_title = DevOps Engineer | Homelab Enthusiast
cv_introduction_0 =
DevOps Engineer und Softwareentwickler mit starkem akademischen Hintergrund
in Netzwerktechnologien und Softwareentwicklung. Spezialisiert auf Kubernetes,
Ansible, Azure und modernen Cloud-Technologien. Praxisnahe
Forschungserfahrung in Software-Defined Networking, 5G und Maschinellem Lernen.
Leidenschaft für effiziente IT-Infrastrukturen, Automatisierung und
innovative Softwarelösungen.
cv_introduction_tools = Lieblingstools:
cv_workexperience_title = Berufserfahrung
cv_workexperience_se1_gefeba_title = Software Entwickler @ gefeba Engineering GmbH
cv_workexperience_se1_gefeba_time = 2013 - 2015
cv_workexperience_se1_gefeba_description = Nach einem Schulpraktikum wurde mir
eine Stelle als Software Entwickler angeboten. Ich arbeitete hauptsächlich
an internen ERP-Projekten, entwickelte ein Tool, das bei der Verwaltung des
projektbezogenen E-Mail-Verkehrs half, und arbeitete an dem internen
Stammdatenmanagement-Tool.
cv_workexperience_student_fse_title = Fachschaftsratsmitglied @ UDE (Ehrenamt)
cv_workexperience_student_fse_time = 2016 - 2019
cv_workexperience_student_fse_description = Als Fachschaftsratsmitglied wirkte
ich in Fakultätsausschüssen mit und organisierte soziale Veranstaltungen.
Meine Hauptaufgaben als Mitglied waren die Verwaltung der IT-Infrastruktur
und die Unterstützung der Studierenden bei organisatorischen und
fachspezifischen Themen.
cv_workexperience_se2_gefeba_title = Software Entwickler @ gefeba Engineering GmbH
cv_workexperience_se2_gefeba_time = 2018 - 2020
cv_workexperience_se2_gefeba_description = Als Software Entwickler bei gefeba
Engineering arbeitete ich an dem Hauptprodukt des Unternehmens, einem
Frame-basierten Datenaustauschsystem zur Überwachung von Industriemaschinen
mit C# und Entity Framework. Ein weiteres Projekt, an dem ich gearbeitet habe,
war eine Echtzeit-Anwendung zur Visualisierung von Protokollen für dieselben
Maschinen.
cv_workexperience_mentoring_ude_title = Mentoring @ UDE
cv_workexperience_mentoring_ude_time = 2021 - 2022
cv_workexperience_mentoring_ude_description = Als Mentor für Studienanfänger
im Studiengang Informatik der Universität Duisburg-Essen habe ich zu Beginn
jedes Semesters Gruppen von ~20 Studienanfängern in ihr neues akademisches
Umfeld eingeführt. Ich bot zusätzliche organisatorische und technische
Unterstützung für das erste Jahr an der Universität an.
cv_workexperience_ra_ude_title = Studentische Hilfskraft @ UDE
cv_workexperience_ra_ude_time = 2021 - 2024
cv_workexperience_ra_ude_description = Während meiner Tätigkeit bei der
Network Communication System Research
Group an der Universität Duisburg-Essen habe ich an der Forschung rund um
Software Defined Networking, 5G, Staukontrollalgorithmen und föderiertes
maschinelles Lernen mitgearbeitet. Ich habe die On-Premise- und Cloud-
Infrastruktur, das Inventarsystem und die Online-Präsenz der Forschungsgruppe
aufgebaut und verwaltet.
cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData
cv_workexperience_dd_devops_time = 2025 - Jetzt
cv_workexperience_dd_devops_description = Als DevOps Engineer bei DextraData
bin ich für die Cloud-basierte Infrastruktur hinter den vielfältigen
SaaS-Produkten des Unternehmens verantwortlich. Ich arbeite eng mit
Software-Entwicklern, Customer Success Managern und Customer Success Engineers
zusammen, um betriebliche Exzellenz und unterbrechungsfreie Servicebereitstellung
für unsere Kunden zu gewährleisten, wobei ihre spezifischen Anforderungen im
Vordergrund stehen. Ich verwalte Kubernetes-Cluster-Deployments, überwache
kontinuierlich Deployment-Gesundheitsmetriken und implementiere
Infrastructure-as-Code-Lösungen.
cv_socials_title = Profile
cv_education_title = Bildungsweg
cv_education_bachelor_title = BSc Angewandte Informatik - Systems Engineering
cv_education_bachelor_time = 2015 - jetzt
cv_skills_title = Fähigkeiten
cv_skills_devops_title = DevOps
cv_skills_devops_ansible = Ansible
cv_skills_devops_kubernetes = Kubernetes
cv_skills_devops_gitops = GitOps
cv_skills_software_engineering_title = Programmiersprachen
cv_skills_software_engineering_rust = Rust
cv_skills_software_engineering_python = Python
cv_skills_software_engineering_csharp = C#
cv_languages_title = Sprachkenntnisse
cv_languages_german = Deutsch (Muttersprache)
cv_languages_english = Englisch (C2)
cv_languages_vietnamese = Vietnamesisch (B1)
cv_languages_japanese = Japanisch (A1)
cv_interests_title = Interessen
cv_interests_coffee = Kaffee
cv_interests_tech_it = Tech/IT
cv_interests_guitar = Gitarre
cv_interests_mechanical_keyboards = Mechanische Tastaturen
publications_projects_publications_title = Veröffentlichungen
publications_projects_publications_rpm_title = RPM: Reverse Path Congestion Marking on P4 Programmable Switches
publications_projects_publications_rpm_authors = N. Baganal-Krishna, T.-D. Tran, R. Kundel and A. Rizk
publications_projects_publications_rpm_conference = IEEE LCN 2023
publications_projects_publications_rpm_url = https://doi.org/10.48550/arXiv.2307.09639
publications_projects_publications_rpm_description = In diesem Artikel stellen
wir Reverse Path Congestion Marking (RPM) vor, um die Reaktion auf
Netzwerküberlastungen zu beschleunigen, ohne den End-Host-Stack zu verändern.
RPM entkoppelt das Stausignal vom nachgelagerten Pfad nach dem Engpass,
während die Stabilität der Staukontrollschleife erhalten bleibt. Wir zeigen,
dass RPM die Durchsatzfairness für RTT bei heterogenen TCP-Flüssen sowie die
Flussabwicklungszeit verbessert, insbesondere für kleine Data Center TCP
(DCTCP)-Flows um P4 programmierbare ASIC-Switches.
publications_projects_publications_iot_fuzzers_title = Overview of IoT Fuzzing Techniques
publications_projects_publications_iot_fuzzers_authors = Tuan-Dat Tran
publications_projects_publications_iot_fuzzers_conference = Seminar
publications_projects_publications_iot_fuzzers_description = In dieser Arbeit
vergleichen wir Methoden, die speziell von IoT Fuzzern genutzt werden um die
von IoT Geräten stammenden Herausforderungen und Einschränkungen zu umgehen.
publications_projects_projects_title = Projekte
publications_projects_projects_bpba_title = Unbenannter Ethereum Smart Contract Fuzzer
publications_projects_projects_bpba_authors = Tuan-Dat Tran
publications_projects_projects_bpba_kind = Bachelorprojekt/Bachelorarbeit
publications_projects_projects_bpba_description = In diesem aktuell laufendem
Projekt entwickle ich einen Ethereum Smart Contract Fuzzer. Mehr Infos folgen...
publications_projects_projects_dotfiles_title = .dotfiles
publications_projects_projects_dotfiles_authors = Tuan-Dat Tran
publications_projects_projects_dotfiles_kind = Personal
publications_projects_projects_dotfiles_description = dotfiles ist ein
umgangssprachlicher Begriff, der normalerweise für Konfigurationsdateien in
Linux-basierten Systemen verwendet wird. Meine Dotfiles enthalten
Konfigurationen für Tools, die ich häufig verwende, sowie eine Dokumentation
zur Einrichtung meines täglich genutzten Notebooks. Sie bieten eine Grundlage
für jedes persönliche Linux-System, das ich einrichte, und ermöglichen
Wiederholbarkeit, was den Prozess der Einrichtung eines Linux-basierten
Systems vereinfacht.
publications_projects_projects_homelab_title = Homelab
publications_projects_projects_homelab_authors = Tuan-Dat Tran
publications_projects_projects_homelab_kind = Personal
publications_projects_projects_homelab_description = Ansible ist ein
Automatisierungs-Werkzeug, die eine automatische Maschinenbereitstellung,
Konfigurationsverwaltung und Anwendungsbereitstellung ermöglicht. Ich
verwende Ansible, um mein Homelab einzurichten, das mir als Plattform zum
Ausprobieren und Lernen neuer Technologien dient.
publications_projects_projects_athome_title = Diese Website
publications_projects_projects_athome_authors = Tuan-Dat Tran
publications_projects_projects_athome_kind = Personal
publications_projects_projects_athome_description = Diese Website ist eine mit
dem auf Rust basiertem Dioxus Framwork und TailwindCSS gebaute Full Stack
WASM Website, die sowohl zum Auffrischen von Web Themen, sowie als Rust
Hobbyprojekt dient.
impressum_on = Impressum
component_under_construction = Diese Seite befindet sich gerade im Aufbau
footer_year = © 2025
footer_name = Tuan-Dat Tran
footer_rights = . All Rights Reserved.
footer_contact = Kontakt
link_opens_new_tab = (öffnet in neuem Tab)

View File

@@ -1,145 +0,0 @@
headers_home = Home
headers_cv = Résumé
headers_publications_projects = Publications/Projects
headers_about = About
headers_language_buttons_english = 🇬🇧 English
headers_language_buttons_german = 🇩🇪 German
home_card_name = Tuan-Dat Tran
home_card_gender = (He/Him)
home_card_text =
Hey there! 👋🏻👋🏼👋🏽👋🏾👋🏿
Welcome to my little place on the internet
My name is Tuan and I'm passionate about Linux, system security, automation, performance tweaking and all things tech.
home_card_contact_button = Get in touch.
cv_introduction_title = DevOps Engineer | Homelab Enthusiast
cv_introduction_0 =
DevOps Engineer and Software Developer with a strong academic background in
networking technologies and software development. Specialized in Kubernetes,
Ansible, Azure, and modern cloud technologies. Hands-on research experience
in Software-Defined Networking, 5G, and Machine Learning. Passionate about
efficient IT infrastructures, automation, and innovative software
solutions.
cv_introduction_tools = Favorite Tools:
cv_workexperience_title = Work Experience
cv_workexperience_se1_gefeba_title = Software Engineer @ gefeba Engineering GmbH
cv_workexperience_se1_gefeba_time = 2013 - 2015
cv_workexperience_se1_gefeba_description = After a school internship I got
offered a job as a Software Engineer. I mostly worked on internal ERP
projects, designed a tool which aided in managing project related mail
traffic and worked on the internal master data management tool.
cv_workexperience_student_fse_title = Student Council Member @ UDE (Volunteer)
cv_workexperience_student_fse_time = 2016 - 2019
cv_workexperience_student_fse_description = As a student council member I
participated in faculty committees and organized social events. My main
responsibilities as a member were the management of the IT infrastructure and
supporting students, be it organizationally or subject-specific.
cv_workexperience_se2_gefeba_title = Software Engineer @ gefeba Engineering GmbH
cv_workexperience_se2_gefeba_time = 2018 - 2020
cv_workexperience_se2_gefeba_description = As a software engineer at gefeba
Engineering I worked on the companies main software product, which was a
frame-based data exchange system to monitor industry machinery using C# and
Entity Framework. Another project I worked on was a real time log
visualization application for the same machinery.
cv_workexperience_mentoring_ude_title = Mentoring @ UDE
cv_workexperience_mentoring_ude_time = 2021 - 2022
cv_workexperience_mentoring_ude_description = As a mentor for students
enrolling into the computer science program of the University Duisburg-Essen I
regularly introduced groups of ~20 freshmen to their new academic environment at the
beginning of each semester. Offering additional organizational and technical
guidance for their first year in university.
cv_workexperience_ra_ude_title = Research Assistant @ UDE
cv_workexperience_ra_ude_time = 2021 - 2024
cv_workexperience_ra_ude_description = While working at the Network
Communication System Research Group at the University Duisburg-Essen as a
research assistant I've assisted in research around software defined
networking, 5G, congestion control algorithms and federated machine learning.
I've established and managed the research groups on-premise and cloud
infractructure, inventory system and online presence.
cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData
cv_workexperience_dd_devops_time = 2025 - now
cv_workexperience_dd_devops_description = As a DevOps Engineer at DextraData,
I'm responsible for the cloud-based infrastructure behind the company's many
SaaS products. I work closely with software developers, Customer Success
Managers, and Customer Success Engineers to ensure operational excellence
and seamless service delivery for our customers, always keeping their
specific needs in mind. My duties include managing Kubernetes cluster
deployments, continuously monitoring deployment health metrics, and
implementing Infrastructure-as-Code solutions.
cv_socials_title = Socials
cv_education_title = Education
cv_education_bachelor_title = BSc Systems Engineering
cv_education_bachelor_time = 2015 - now
cv_skills_title = Skills
cv_skills_devops_title = DevOps
cv_skills_devops_ansible = Ansible
cv_skills_devops_kubernetes = Kubernetes
cv_skills_devops_gitops = GitOps
cv_skills_software_engineering_title = Software Engineering
cv_skills_software_engineering_rust = Rust
cv_skills_software_engineering_python = Python
cv_skills_software_engineering_csharp = C#
cv_languages_title = Languages
cv_languages_german = German (Native)
cv_languages_english = English (C2)
cv_languages_vietnamese = Vietnamese (B1)
cv_languages_japanese = Japanese (A1)
cv_interests_title = Interests
cv_interests_coffee = Coffee
cv_interests_tech_it = Tech/IT
cv_interests_guitar = Guitar
cv_interests_mechanical_keyboards = Mechanical Keyboards
publications_projects_publications_title = Publications
publications_projects_publications_rpm_title = RPM: Reverse Path Congestion Marking on P4 Programmable Switches
publications_projects_publications_rpm_authors = N. Baganal-Krishna, T.-D. Tran, R. Kundel and A. Rizk
publications_projects_publications_rpm_conference = IEEE LCN 2023
publications_projects_publications_rpm_url = https://doi.org/10.48550/arXiv.2307.09639
publications_projects_publications_rpm_description = In this paper, we present
Reverse Path Congestion Marking (RPM) to accelerate the reaction to network
congestion events without changing the end-host stack. RPM decouples the
congestion signal from the downstream path after the bottleneck while
maintaining the stability of the congestion control loop. We show that RPM
improves throughput fairness for RTT on heterogeneous TCP flows as well as
the flow completion time, especially for small Data Center TCP (DCTCP) flows
around P4 programmable ASIC switches.
publications_projects_publications_iot_fuzzers_title = Overview of IoT Fuzzing Techniques
publications_projects_publications_iot_fuzzers_authors = Tuan-Dat Tran
publications_projects_publications_iot_fuzzers_conference = Seminar
publications_projects_publications_iot_fuzzers_description = In this paper, we
are comparing techniques used by IoT fuzzers to circumvent the challenges
presented by IoT devices and the constraints of the solutions proposed by the
IoT fuzzers.
publications_projects_projects_title = Projects
publications_projects_projects_bpba_title = Undisclosed Ethereum Smart Contract Fuzzer
publications_projects_projects_bpba_authors = Tuan-Dat Tran
publications_projects_projects_bpba_kind = Bachelor Project/Bachelor Thesis
publications_projects_projects_bpba_description = In this ongoing project I am building an Ethereum Smart Contract Fuzzer. More info will follow.
publications_projects_projects_dotfiles_title = .dotfiles
publications_projects_projects_dotfiles_authors = Tuan-Dat Tran
publications_projects_projects_dotfiles_kind = Personal
publications_projects_projects_dotfiles_description = dotfiles is a slang term
usually used for configuration files in Linux based systems. My dotfiles
contain configurations for tools I frequently use as well as a documentation
on how to set up my daily-use notebook. It provides a baseline for any
personal Linux system I set up and allows for repeatability simplifies the
process of setting up an ArchLinux based system.
publications_projects_projects_homelab_title = Homelab
publications_projects_projects_homelab_authors = Tuan-Dat Tran
publications_projects_projects_homelab_kind = Personal
publications_projects_projects_homelab_description = Ansible is a automation
tool which allows for automatic provisioning, configuration management and
application deployment. I use ansible to set up my homelab, which serves as a
platform for me to try out and learn new technologies.
publications_projects_projects_athome_title = This Website
publications_projects_projects_athome_authors = Tuan-Dat Tran
publications_projects_projects_athome_kind = Personal
publications_projects_projects_athome_description = This website is a
full-stack WASM site built using the Rust-based Dioxus framework and
TailwindCSS. It serves both as a way to refresh web development topics and as
a Rust hobby project.
impressum_on = Impressum
component_under_construction = This page is currently under construction
footer_year = © 2025
footer_name = Tuan-Dat Tran
footer_rights = . All Rights Reserved.
footer_contact = Contact
link_opens_new_tab = (opens in a new tab)

View File

@@ -1,3 +0,0 @@
[toolchain]
channel = "1.86.0"
components = ["rustfmt", "clippy", "rust-analyzer"]

View File

@@ -1,61 +0,0 @@
import os
import re
def check_i18n_keys(project_root):
src_dir = os.path.join(project_root, "src")
languages_dir = os.path.join(project_root, "languages")
rust_key_regex = re.compile(r't!\("([a-zA-Z0-9_.]+)"\)')
ftl_key_regex = re.compile(r"^([a-zA-Z0-9_.-]+)\s*=")
used_keys = set()
defined_keys = set()
# Extract keys from Rust files
for root, _, files in os.walk(src_dir):
for file in files:
if file.endswith(".rs"):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
for match in rust_key_regex.finditer(content):
used_keys.add(match.group(1))
# Extract keys from FTL files
for root, _, files in os.walk(languages_dir):
for file in files:
if file.endswith(".ftl"):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
match = ftl_key_regex.match(line)
if match:
defined_keys.add(match.group(1))
print("--- i18n Key Check Report ---")
missing_keys = used_keys - defined_keys
if not missing_keys:
print("✅ No missing translation keys found in FTL files.")
else:
print(
"❌ Missing translation keys (used in code but not defined in FTL files):"
)
for key in sorted(list(missing_keys)):
print(f" - {key}")
unused_keys = defined_keys - used_keys
if not unused_keys:
print("✅ No unused translation keys found in FTL files.")
else:
print("⚠️ Unused translation keys (defined in FTL files but not used in code):")
for key in sorted(list(unused_keys)):
print(f" - {key}")
print("-----------------------------")
if __name__ == "__main__":
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
check_i18n_keys(project_root)

View File

@@ -1,6 +1,6 @@
#!/bin/sh
TAG=$(git describe --exact-match --tags)
TAG=$(git branch | grep "*" | awk '{ print $2 }')
LOCAL_IMAGE="athome"
REGISTRY="mos4"
REMOTE_IMAGE="athome"

View File

@@ -1,5 +1,5 @@
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
#[derive(PartialEq, Props, Clone)]
pub struct PProps {
@@ -14,7 +14,7 @@ pub fn P(props: PProps) -> Element {
div {
class: "{props.class}",
p {
class: "mb-2 font-normal text-white",
class: "mb-2 font-normal text-gray-900 dark:text-white",
{props.children}
},
}
@@ -27,7 +27,7 @@ pub fn Title(props: PProps) -> Element {
div {
class: "{props.class}",
h1 {
class: "mb-2 text-2xl font-bold tracking-tight text-white",
class: "mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white",
{props.children}
}
}
@@ -40,7 +40,7 @@ pub fn H1(props: PProps) -> Element {
div {
class: "{props.class}",
h1 {
class: "mb-4 text-3xl font-extrabold text-white md:text-5xl lg:text-6xl",
class: "mb-4 text-3xl font-extrabold text-gray-900 dark:text-white md:text-5xl lg:text-6xl",
span {
class: "text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400",
{props.children}
@@ -56,7 +56,7 @@ pub fn H4(props: PProps) -> Element {
class: "{props.class}",
class: "mb-4",
h3 {
class: "text-lg uppercase text-cyan-400 tracking-widest mb-4 font-bold",
class: "text-lg uppercase text-cyan-600 dark:text-cyan-400 tracking-widest mb-4 font-bold",
{props.children}
}
}
@@ -69,7 +69,7 @@ pub fn H5(props: PProps) -> Element {
div {
class: "{props.class}",
h5 {
class: "mb-2 text-2xl font-bold tracking-tight text-white",
class: "mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white",
{props.children}
}
}
@@ -89,7 +89,7 @@ pub struct CardProp {
pub fn Card(prop: CardProp) -> Element {
rsx! {
div {
class: "flex flex-col py-10 items-center min-w-fit text-white bg-gradient-to-r from-purple-500 to-blue-500 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-800 rounded-lg px-5 py-2.5 min-w-fit max-w-8",
class: "flex flex-col py-10 items-center min-w-fit text-white bg-gradient-to-r from-purple-500 to-blue-500 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 rounded-lg px-5 py-2.5 min-w-fit max-w-8",
div {
class: "pb-4",
div {
@@ -97,7 +97,7 @@ pub fn Card(prop: CardProp) -> Element {
Picture {src: "{prop.picture}"},
}
}
Title { "{prop.name}", span { class: "text-grey-500 text-lg", " {prop.gender}" } },
Title { "{prop.name}", span { class: "text-grey-600 dark:text-grey-500 text-lg", " {prop.gender}" } },
{ prop.children }
}
}
@@ -121,14 +121,16 @@ fn Picture(prop: PictureProp) -> Element {
}
pub fn UnderConstruction() -> Element {
let i18 = use_i18();
rsx! {
div {
class:"rounded justify-between w-full p-4 border-b bg-gray-700 border-gray-600 my-8",
class:"rounded justify-between w-full p-4 border-b border-gray-200 bg-gray-50 dark:bg-gray-700 dark:border-gray-600 my-8",
div {
class:"items-center mx-auto",
p {
class:"items-center text-sm font-normal text-gray-400",
span { { t!("component_under_construction") } }
class:"items-center text-sm font-normal text-gray-500 dark:text-gray-400",
span { { translate!(i18, "components.under_construction") } }
}
}
}
@@ -137,178 +139,6 @@ pub fn UnderConstruction() -> Element {
pub fn HR() -> Element {
rsx! {
hr { class:"h-px my-8 border-0 bg-gray-700"}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct BoldingProp {
authors: String,
patterns: Vec<String>,
}
pub fn Bolding(prop: BoldingProp) -> Element {
let mut elements = vec![];
let mut last_index = 0;
let authors_text = &prop.authors;
let mut matches: Vec<(usize, usize, &str)> = vec![];
for pattern in &prop.patterns {
for (start, _) in authors_text.match_indices(pattern) {
matches.push((start, start + pattern.len(), pattern));
}
}
matches.sort_by_key(|(start, _, _)| *start);
for (start, end, matched_pattern) in matches {
if last_index < start {
elements.push(rsx! { "{&authors_text[last_index..start]}" });
}
elements.push(rsx! { b { "{matched_pattern}" } });
last_index = end;
}
if last_index < authors_text.len() {
elements.push(rsx! { "{&authors_text[last_index..]}" });
}
rsx! {
div {
P {
for i in elements {
{i}
}
}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct UrlingProp {
#[props(default = "".to_string())]
class: String,
text: String,
patterns: Vec<String>,
url: String,
#[props(default = true)]
new_tab: bool,
}
pub fn Urling(prop: UrlingProp) -> Element {
let mut elements = vec![];
let mut last_index = 0;
let text = &prop.text;
let mut matches: Vec<(usize, usize, &str)> = vec![];
for pattern in &prop.patterns {
for (start, _) in text.match_indices(pattern) {
matches.push((start, start + pattern.len(), pattern));
}
}
matches.sort_by_key(|(start, _, _)| *start);
for (start, end, matched_pattern) in matches {
if last_index < start {
elements.push(rsx! { "{&text[last_index..start]}" });
}
elements.push(rsx! {
Link {
class: "{prop.class}",
to: "{prop.url}",
new_tab: prop.new_tab,
"{matched_pattern}"
}
});
last_index = end;
}
if last_index < text.len() {
elements.push(rsx! { "{&text[last_index..]}" });
}
rsx! {
div {
P {
for i in elements {
{i}
}
}
}
}
}
#[component]
pub fn BadgeList(list: Vec<String>) -> Element {
rsx!(
ul {
class: "flex flex-wrap gap-2",
for (index, value) in list.iter().enumerate() {
li { key: "{index}", RandomBadge { text: "{value}"} }
}
}
)
}
#[component]
fn RandomBadge(text: String) -> Element {
let badge_color = random_badge_color(text.len());
rsx! {
span {
class:"text-xs font-medium me-2 px-2.5 py-0.5 rounded {badge_color}",
"{text}"
}
}
}
fn random_badge_color(seed: usize) -> String {
let colors = [
"bg-blue-900 text-blue-300",
"bg-gray-700 text-gray-300",
"bg-red-900 text-red-300",
"bg-green-900 text-green-300",
"bg-yellow-900 text-yellow-300",
"bg-indigo-900 text-indigo-300",
"bg-purple-900 text-purple-300",
"bg-pink-900 text-pink-300",
];
colors[seed % colors.len()].to_string()
}
#[derive(PartialEq, Props, Clone)]
pub struct AccessibleLinkProps {
to: String,
#[props(default = "".to_string())]
class: String,
#[props(default = false)]
new_tab: bool,
children: Element,
}
#[component]
pub fn AccessibleLink(props: AccessibleLinkProps) -> Element {
let mut aria_label = String::new();
let mut rel = String::new();
if props.new_tab {
aria_label = t!("link_opens_new_tab").to_string();
rel = "noopener noreferrer".to_string();
}
rsx! {
Link {
to: "{props.to}",
class: "{props.class}",
new_tab: props.new_tab,
rel: "{rel}",
aria_label: "{aria_label}",
{props.children}
}
hr { class:"h-px my-8 bg-gray-200 border-0 dark:bg-gray-700"}
}
}

10
src/consulting.rs Normal file
View File

@@ -0,0 +1,10 @@
use dioxus::prelude::*;
use crate::components::UnderConstruction;
#[component]
pub fn Consulting() -> Element {
rsx! {
UnderConstruction { },
}
}

181
src/cv.rs
View File

@@ -1,84 +1,69 @@
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
use crate::components::*;
use crate::components::{H4, HR};
#[component]
pub fn CV() -> Element {
rsx! {
div {
class: "flex flex-col",
div {
class: "flex flex-col sm:flex-row justify-center items-center sm:space-x-8 space-y-8 sm:space-y-0",
img {
class: "rounded-full w-24 h-24",
alt: "headshot",
src: asset!("./assets/pictures/headshot.webp")
}
Introduction {},
Socials {}
},
class: "flex flex-col ",
Introduction {},
HR {}
div {
class: "flex flex-col justify-between sm:flex-row",
class: "flex justify-between",
WorkExperience {},
Miscellaneous {},
},
HR {},
Socials {}
}
}
}
fn Introduction() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "flex-col",
h6 { class: "text-lg font-semibold text-white", { t!("cv_introduction_title") } },
P { { t!("cv_introduction_0") } },
P {
{ t!("cv_introduction_tools") },
" ",
AccessibleLink { new_tab: true, to: "https://www.lazyvim.org/", "NeoVim (LazyVim)" },
", ",
AccessibleLink { new_tab: true, to: "https://zellij.dev/", "Zellij" },
", ",
AccessibleLink { new_tab: true, to: "https://k9scli.io/", "k9s" }
},
},
class: "flex",
img {
class: "rounded-full w-16 h-16 mx-16",
src: "/pictures/headshot.webp"
}
P { { translate!(i18, "cv.introduction") }}
}
}
}
fn WorkExperience() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "ms-8 max-w-3/4",
H4 { { t!("cv_workexperience_title") } },
H4 { { translate!(i18, "cv.workexperience.title") } },
ol {
class:"relative border-s border-gray-700",
CVEntry {time: t!("cv_workexperience_dd_devops_time"), title: t!("cv_workexperience_dd_devops_title"),
technologies: vec!["Kubernetes".to_string(), "ArgoCD".to_string(), "Ansible".to_string(), "Azure".to_string(), "ELK".to_string(), "Helm".to_string()],
description: t!("cv_workexperience_dd_devops_description")
},
CVEntry {time: t!("cv_workexperience_ra_ude_time"), title: t!("cv_workexperience_ra_ude_title"),
class:"relative border-s border-gray-200 dark:border-gray-700",
CVEntry {time: translate!(i18, "cv.workexperience.ra_ude.time"), title: translate!(i18, "cv.workexperience.ra_ude.title"),
technologies: vec!["Rust".to_string(), "Python".to_string(), "P4".to_string(), "Linux".to_string(), "Docker".to_string(), "Kubernetes".to_string()],
description: t!("cv_workexperience_ra_ude_description")
description: translate!(i18, "cv.workexperience.ra_ude.description")
},
CVEntry {time: t!("cv_workexperience_mentoring_ude_time"), title: t!("cv_workexperience_mentoring_ude_title"),
CVEntry {time: translate!(i18, "cv.workexperience.mentoring_ude.time"), title: translate!(i18, "cv.workexperience.mentoring_ude.title"),
technologies: vec!["Powerpoint".to_string()],
description: t!("cv_workexperience_mentoring_ude_description")
description: translate!(i18, "cv.workexperience.mentoring_ude.description")
},
CVEntry {time: t!("cv_workexperience_se2_gefeba_time"), title: t!("cv_workexperience_se2_gefeba_title"),
CVEntry {time: translate!(i18, "cv.workexperience.se2_gefeba.time"), title: translate!(i18, "cv.workexperience.se2_gefeba.title"),
technologies: vec!["C#".to_string(), "Angular".to_string(), "bootstrap".to_string(), "Entity Framework".to_string()],
description: t!("cv_workexperience_se2_gefeba_description")
description: translate!(i18, "cv.workexperience.se2_gefeba.description")
},
CVEntry {time: t!("cv_workexperience_student_fse_time"), title: t!("cv_workexperience_student_fse_title"),
CVEntry {time: translate!(i18, "cv.workexperience.student_fse.time"), title: translate!(i18, "cv.workexperience.student_fse.title"),
technologies: vec!["Linux".to_string(), "Networking".to_string(), "LaTeX".to_string()],
description: t!("cv_workexperience_student_fse_description")
description: translate!(i18, "cv.workexperience.student_fse.description")
},
CVEntry {time: t!("cv_workexperience_se1_gefeba_time"), title: t!("cv_workexperience_se1_gefeba_title"),
CVEntry {time: translate!(i18, "cv.workexperience.se1_gefeba.time"), title: translate!(i18, "cv.workexperience.se1_gefeba.title"),
technologies: vec!["C#".to_string(), "HTML".to_string(), "Javascript".to_string(), "CSS".to_string()],
description: t!("cv_workexperience_se1_gefeba_description")
description: translate!(i18, "cv.workexperience.se1_gefeba.description")
},
}
},
@@ -98,60 +83,67 @@ fn Miscellaneous() -> Element {
}
fn Education() -> Element {
let i18 = use_i18();
rsx! {
div {
H4 { { t!("cv_education_title") } },
H4 { { translate!(i18, "cv.education.title") } },
Entry {
title: t!("cv_education_bachelor_title"),
time { class:"mb-1 text-sm font-normal leading-none text-gray-500", { t!("cv_education_bachelor_time") } },
title: translate!(i18, "cv.education.bachelor.title"),
time { class:"mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500", { translate!(i18, "cv.education.bachelor.time") } },
},
}
}
}
fn Skills() -> Element {
let i18 = use_i18();
rsx! {
div {
H4 { { t!("cv_skills_title") }},
H4 { { translate!(i18, "cv.skills.title") }},
Entry {
title: t!("cv_skills_devops_title"),
P { { t!("cv_skills_devops_ansible") } },
P { { t!("cv_skills_devops_kubernetes") } },
P { { t!("cv_skills_devops_gitops") } },
}
Entry {
title: t!("cv_skills_software_engineering_title"),
P { { t!("cv_skills_software_engineering_rust") } },
P { { t!("cv_skills_software_engineering_python") } },
P { { t!("cv_skills_software_engineering_csharp") } },
title: translate!(i18, "cv.skills.devops.title"),
P { { translate!(i18, "cv.skills.devops.ansible") } },
P { { translate!(i18, "cv.skills.devops.kubernetes") } },
P { { translate!(i18, "cv.skills.devops.gitops") } },
}Entry {
title: translate!(i18, "cv.skills.software_engineering.title"),
P { { translate!(i18, "cv.skills.software_engineering.rust") } },
P { { translate!(i18, "cv.skills.software_engineering.python") } },
P { { translate!(i18, "cv.skills.software_engineering.csharp") } },
}
}
}
}
fn Languages() -> Element {
let i18 = use_i18();
rsx! {
div {
H4 { { t!("cv_languages_title") } },
H4 { { translate!(i18, "cv.languages.title") } },
Entry {
P { {t!("cv_languages_german") } },
P { {t!("cv_languages_english") } },
P { {t!("cv_languages_vietnamese") } },
P { {t!("cv_languages_japanese") } },
P { {translate!(i18, "cv.languages.german") } },
P { {translate!(i18, "cv.languages.english") } },
P { {translate!(i18, "cv.languages.vietnamese") } },
P { {translate!(i18, "cv.languages.japanese") } },
}
}
}
}
fn Interests() -> Element {
let i18 = use_i18();
rsx! {
div {
H4 { { t!("cv_interests_title") } },
H4 { { translate!(i18, "cv.interests.title") } },
Entry {
P { { t!("cv_interests_coffee") } },
P { { t!("cv_interests_tech_it") } },
P { { t!("cv_interests_guitar") } },
P { { t!("cv_interests_mechanical_keyboards") } },
P { { translate!(i18, "cv.interests.coffee") } },
P { { translate!(i18, "cv.interests.tech_it") } },
P { { translate!(i18, "cv.interests.guitar") } },
P { { translate!(i18, "cv.interests.mechanical_keyboards") } },
}
}
}
@@ -170,7 +162,7 @@ fn Entry(props: EntryProps) -> Element {
rsx! {
div {
class: "{props.class}",
h6 { class: "font-semibold text-white", "{props.title}"}
h6 { class: "font-semibold text-gray-900 dark:text-white", "{props.title}"}
{props.children},
}
}
@@ -193,25 +185,52 @@ fn CVEntry(props: CVEntryProps) -> Element {
li {
class: "max-w-xl",
class: "{props.class}",
div { class:"absolute w-3 h-3 rounded-full mt-1.5 -start-1.5 border border-gray-900 bg-gray-700"},
time { class:"mb-1 text-sm font-normal leading-none text-gray-500", "{props.time}"},
h6 { class: "text-lg font-semibold text-white", "{props.title}"}
BadgeList{ list: props.technologies }
p { class:"text-base font-normal text-gray-400", "{props.description}"},
div { class:"absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700"},
time { class:"mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500", "{props.time}"},
h6 { class: "text-lg font-semibold text-gray-900 dark:text-white", "{props.title}"}
ul {
class: "flex",
for (index, value) in props.technologies.iter().enumerate() {
li { key: "{index}", RandomBadge { text: "{value}"} }
}
}
p { class:"text-base font-normal text-gray-500 dark:text-gray-400", "{props.description}"},
{props.children}
}
}
}
#[component]
fn RandomBadge(text: String) -> Element {
let badge_color = random_badge_color(text.len());
rsx! {
span {
class:"text-xs font-medium me-2 px-2.5 py-0.5 rounded ",
class: "{badge_color}",
"{text}"
}
}
}
fn random_badge_color(seed: usize) -> String {
let colors = [
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
"bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300",
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
"bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
];
colors[seed % colors.len()].to_string()
}
fn Socials() -> Element {
rsx! {
div {
class: "ms-8 max-w-3/4",
H4 { { t!("cv_socials_title") } },
div {
class: "flex justify-center items-center space-x-4",
P { AccessibleLink { to:"https://www.linkedin.com/in/tudattr/", class:"hover:underline", new_tab: true, img { class: "h-8", src:asset!("./assets/pictures/LI-Bug.svg.original.svg"), alt:"LinkedIn Logo" } }},
}
"todo!()"
}
}
}
@@ -220,7 +239,7 @@ fn Socials() -> Element {
fn P(children: Element) -> Element {
rsx! {
p {
class: "text-base font-normal text-gray-400",
class: "text-base font-normal text-gray-500 dark:text-gray-400",
{children},
}
}

View File

@@ -1,30 +1,37 @@
use crate::components::{AccessibleLink, Card, P};
use crate::components::{Card, P};
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
#[component]
pub fn Home() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "container mx-auto p-4 flex items-center justify-center max-w-md w-full",
Card {
name: t!("home_card_name"),
gender: t!("home_card_gender"),
picture: asset!("./assets/pictures/headshot.webp"),
name: translate!(i18, "home.card.name"),
gender: translate!(i18, "home.card.gender"),
picture: "/pictures/headshot.webp",
div {
class: "py-4",
div {
class: "mb-2",
for line in t!("home_card_text").split("\n").into_iter() {
P {{line}}
}
P { { translate!(i18, "home.card.l1") } },
P { { translate!(i18, "home.card.l2") } },
P { { translate!(i18, "home.card.l3") },
Link {
to: "https://git.tudattr.dev/explore/repos",
new_tab: true,
class: "items-center font-medium hover:underline",
"gitea"},
{ translate!(i18, "home.card.l3_1") }
},
},
},
AccessibleLink {
Link {
to: "mailto:tuan-dat.tran@tudattr.dev",
class: "text-gray-900 bg-gradient-to-br from-green-400 to-blue-600 group-hover:from-green-400 group-hover:to-blue-600 hover:text-white rounded-full shadow-lg py-4 px-4",
new_tab: true,
{ t!("home_card_contact_button") }
{ translate!(i18, "home.card.contact_button") }
}
},
}

View File

@@ -1,59 +1,48 @@
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
use tracing::info;
use crate::components::{H1, HR, P};
#[component]
pub fn Impressum() -> Element {
let mut impressum = use_signal(Vec::<String>::new);
let mut contact = use_signal(Vec::<String>::new);
let mut show_impressum = use_signal(|| false);
let i18 = use_i18();
rsx! {
div {
if show_impressum() {
div {
class: "flex flex-col items-center",
button {
onclick: move |_| async move {
if let Ok(data) = get_impressum().await {
impressum.set(data.clone());
}
if let Ok(data) = get_contact().await {
contact.set(data.clone());
}
div {
class: "flex flex-col items-center",
button {
onclick: move |_| {
info!("Hide impressum.");
},
H1 { { translate!(i18, "impressum.on") } },
},
H1 { { t!("impressum_on") } },
P { "Tuan-Dat Tran" },
P { "c/o AutorenServices.de" },
P { "Birkenallee 24" },
P { "36037 Fulda" },
},
},
div {
class: "flex flex-col items-center",
for line in impressum() {
P { {line} }
HR {}
div {
class: "flex flex-col items-center",
P { "tuan-dat.tran@tudattr.dev" },
P { "+49 176 83468388" },
}
}
if !impressum.read().is_empty() { HR{} },
} else {
div {
class: "flex flex-col items-center",
for line in contact() {
P { {line} }
}
class: "flex flex-col items-center p-3",
button {
onclick: move |_| async move {
show_impressum.set(true);
},
H1 { { translate!(i18, "impressum.off") } },
},
}
}
}
}
#[server(GetServerData)]
async fn get_impressum() -> Result<Vec<String>, ServerFnError> {
Ok(vec![
"Tuan-Dat Tran".to_string(),
"c/o AutorenServices.de".to_string(),
"Birkenallee 24".to_string(),
"36037 Fulda".to_string(),
])
}
async fn get_contact() -> Result<Vec<String>, ServerFnError> {
Ok(vec![
"tuan-dat.tran(at)tudattr(dot)dev".to_string(),
"+49 17(six) 83(four)683(eight)8".to_string(),
])
}

2
src/languages.rs Normal file
View File

@@ -0,0 +1,2 @@
pub static EN_GB: &str = include_str!("./languages/en-GB.json");
pub static DE_DE: &str = include_str!("./languages/de-DE.json");

151
src/languages/de-DE.json Normal file
View File

@@ -0,0 +1,151 @@
{
"id": "de-DE",
"texts": {
"headers": {
"home": "Home",
"cv": "Lebenslauf",
"publications_projects": "Artikel/Projekte",
"consulting": "Consulting",
"about": "Impressum",
"language_buttons": {
"english": "🇬🇧 Englisch",
"german": "🇩🇪 Deutsch"
}
},
"home": {
"card": {
"name": "Tuan-Dat Tran",
"gender": "",
"l1": "Hallihallo! 👋🏻👋🏼👋🏽👋🏾👋🏿",
"l2": "Willkommen auf meiner kleinen Webseite im World Wide Web.",
"l3": "Während du hier bist, schau dir doch meine Projekte auf ",
"l3_1": " an.",
"contact_button": "Get in touch."
}
},
"cv": {
"introduction": "Während meines Bachelorstudiums habe ich viele Erfahrungen in der Industrie und im Studium gesammelt. Meine beruflichen und persönlichen Interessen sind DevOps/IaC, Systems/Software Security und Computer Networking. All diese Interessen vertiefe ich in persönlichen Projekten wie meinem Homelab und CTF-Challenges.",
"workexperience": {
"title": "Berufserfahrung",
"se1_gefeba": {
"title": "Software Entwickler @ gefeba Engineering GmbH",
"time": "2013 - 2015",
"description": "Nach einem Schulpraktikum wurde mir eine Stelle als Software Entwickler angeboten. Ich arbeitete hauptsächlich an internen ERP-Projekten, entwickelte ein Tool, das bei der Verwaltung des projektbezogenen E-Mail-Verkehrs half, und arbeitete an dem internen Stammdatenmanagement-Tool."
},
"student_fse": {
"title": "Fachschaftsratsmitglied @ UDE",
"time": "2016 - 2019",
"description": "Als Fachschaftsratsmitglied wirkte ich in Fakultätsausschüssen mit und organisierte soziale Veranstaltungen. Meine Hauptaufgaben als Mitglied waren die Verwaltung der IT-Infrastruktur und die Unterstützung der Studierenden, sei es organisatorisch oder fachspezifisch."
},
"se2_gefeba": {
"title": "Software Entwickler @ gefeba Engineering GmbH",
"time": "2018 - 2020",
"description": "Als Software Entwickler bei gefeba Engineering arbeitete ich an dem Hauptprodukt des Unternehmens, einem Frame-basierten Datenaustauschsystem zur Überwachung von Industriemaschinen mit C# und Entity Framework. Ein weiteres Projekt, an dem ich gearbeitet habe, war eine Echtzeit-Anwendung zur Visualisierung von Protokollen für dieselben Maschinen."
},
"mentoring_ude": {
"title": "Mentoring @ UDE",
"time": "2021 - 2022",
"description": "Als Mentor für Studienanfänger im Studiengang Informatik der Universität Duisburg-Essen habe ich zu Beginn jedes Semesters Gruppen von ~20 Studienanfängern in ihr neues akademisches Umfeld eingeführt. Ich bot zusätzliche organisatorische und technische Unterstützung für das erste Jahr an der Universität an."
},
"ra_ude": {
"title": "Studentische Hilfskraft @ UDE",
"time": "2021 - jetzt",
"description": "Während meiner Tätigkeit als wissenschaftlicher Mitarbeiter in der Network Communication System Research Group an der Universität Duisburg-Essen habe ich an der Forschung rund um Software Defined Networking, 5G, Staukontrollalgorithmen und föderiertes maschinelles Lernen mitgearbeitet. Ich habe die On-Premise- und Cloud-Infrastruktur, das Inventarsystem und die Online-Präsenz der Forschungsgruppe aufgebaut und verwaltet."
}
},
"education": {
"title": "Bildungsweg",
"bachelor": {
"title": "BSc Angewandte Informatik - Systems Engineering",
"time": "2015 - jetzt",
"description": ""
}
},
"skills": {
"title": "Fähigkeiten",
"devops": {
"title": "DevOps",
"ansible": "Ansible",
"kubernetes": "Kubernetes",
"gitops": "GitOps"
},
"software_engineering": {
"title": "Programmiersprachen",
"rust": "Rust",
"python": "Python",
"csharp": "C#"
}
},
"languages": {
"title": "Sprachkenntnisse",
"german": "Deutsch (Muttersprache)",
"english": "Englisch (C2)",
"vietnamese": "Vietnamesisch (B1)",
"japanese": "Japanisch (A1)"
},
"interests": {
"title": "Interessen",
"coffee": "Kaffee",
"tech_it": "Tech/IT",
"guitar": "Gitarre",
"mechanical_keyboards": "Mechanische Tastaturen"
}
},
"publications_projects": {
"publications": {
"title": "Veröffentlichungen",
"rpm": {
"title": "RPM: Reverse Path Congestion Marking on P4 Programmable Switches",
"authors": "N. Baganal-Krishna, T.-D. Tran, R. Kundel and A. Rizk",
"conference": "IEEE LCN 2023",
"url": "https://doi.org/10.48550/arXiv.2307.09639",
"description": "In diesem Artikel stellen wir Reverse Path Congestion Marking (RPM) vor, um die Reaktion auf Netzwerküberlastungen zu beschleunigen, ohne den End-Host-Stack zu verändern. RPM entkoppelt das Stausignal vom nachgelagerten Pfad nach dem Engpass, während die Stabilität der Staukontrollschleife erhalten bleibt. Wir zeigen, dass RPM die Durchsatzfairness für RTT bei heterogenen TCP-Flüssen sowie die Flussabwicklungszeit verbessert, insbesondere für kleine Data Center TCP (DCTCP)-Flows um P4 programmierbare ASIC-Switches."
},
"iot_fuzzers": {
"title": "Overview of IoT Fuzzing Techniques",
"authors": "Tuan-Dat Tran",
"conference": "Seminar",
"url": "https://git.tudattr.dev/AISE/seminar/src/branch/main/paper.pdf",
"description": "In dieser Arbeit vergleichen wir Methoden, die speziell von IoT Fuzzern genutzt werden um die von IoT Geräten stammenden Herausforderungen und Einschränkungen zu umgehen."
}
},
"projects": {
"title": "Projekte",
"bachelorproject": {
"title": "Unbenannter Ethereum Smart Contract Fuzzer",
"authors": "Tuan-Dat Tran",
"kind": "Bachelorprojekt",
"url": "https://git.ude-syssec.de/uni-due-syssec/students/2022_tuan-dat_tran_libAFLEVMFuzzer/ethfuzz/",
"description": "In diesem aktuell laufendem Projekt entwickle ich einen Ethereum Smart Contract Fuzzer. Mehr Infos folgen..."
},
"dotfiles": {
"title": ".dotfiles",
"authors": "Tuan-Dat Tran",
"kind": "Personal",
"url": "https://git.tudattr.dev/tudattr/dotfiles",
"description": "dotfiles ist ein umgangssprachlicher Begriff, der normalerweise für Konfigurationsdateien in Linux-basierten Systemen verwendet wird. Meine Dotfiles enthalten Konfigurationen für Tools, die ich häufig verwende, sowie eine Dokumentation zur Einrichtung meines täglich genutzten Notebooks. Sie bieten eine Grundlage für jedes persönliche Linux-System, das ich einrichte, und ermöglichen Wiederholbarkeit, was den Prozess der Einrichtung eines ArchLinux-basierten Systems vereinfacht."
},
"homelab": {
"title": "Homelab",
"authors": "Tuan-Dat Tran",
"kind": "Personal",
"url": "https://git.tudattr.dev/tudattr/ansible",
"description": "Ansible ist eine Automatisierungs-Engine, die eine automatische Bereitstellung, Konfigurationsverwaltung und Anwendungsbereitstellung ermöglicht. Ich verwende Ansible, um mein Homelab einzurichten, das mir als Plattform zum Ausprobieren und Lernen neuer Technologien dient."
}
}
},
"impressum": {
"off": "Impressum anzeigen",
"on": "Impressum"
},
"components": {
"under_construction": "Diese Seite befindet sich gerade im Aufbau"
},
"footer": {
"year": "© 2024 ",
"name": "Tuan-Dat Tran",
"rights": ". All Rights Reserved.",
"contact": "Kontakt"
}
}
}

151
src/languages/en-GB.json Normal file
View File

@@ -0,0 +1,151 @@
{
"id": "en-GB",
"texts": {
"headers": {
"home": "Home",
"cv": "CV",
"publications_projects": "Publications/Projects",
"consulting": "Consulting",
"about": "About",
"language_buttons": {
"english": "🇬🇧 English",
"german": "🇩🇪 German"
}
},
"home": {
"card": {
"name": "Tuan-Dat Tran",
"gender": "(He/Him)",
"l1": "Hey there! 👋🏻👋🏼👋🏽👋🏾👋🏿",
"l2": "Welcome to my little place on the internet.",
"l3": "While you're here, why don't you check out my projects over on ",
"l3_1": "?",
"contact_button": "Get in touch."
}
},
"cv": {
"introduction": "While studying for my bachelors degree I accumulated a lot of industry and academic experience. My professional and personal intererests are DevOps/IaC, Systems/Software Security and Computer Networking. All of which I deepen in personal projects such as my homelab and CTF challenges.",
"workexperience": {
"title": "Work Experience",
"se1_gefeba": {
"title": "Software Engineer @ gefeba Engineering GmbH",
"time": "2013 - 2015",
"description": "After a school internship I got offered a job as a Software Engineer. I mostly worked on internal ERP projects, designed a tool which aided in managing project related mail traffic and worked on the internal master data management tool."
},
"student_fse": {
"title": "Student Council Member @ UDE",
"time": "2016 - 2019",
"description": "As a student council member I participated in faculty committees and organized social events. My main responsibilities as a member were the management of the IT infrastructure and supporting students, be it organizationally or subject-specific."
},
"se2_gefeba": {
"title": "Software Engineer @ gefeba Engineering GmbH",
"time": "2018 - 2020",
"description": "As a software engineer at gefeba Engineering I worked on the companies main software product, which was a frame-based data exchange system to monitor industry machinery using C# and Entity Framework. Another project I worked on was a real time log visualization application for the same machinery."
},
"mentoring_ude": {
"title": "Mentoring @ UDE",
"time": "2021 - 2022",
"description": "As a mentor for students enrolling into the computer science program of the University Duisburg-Essen I introduced groups of ~20 freshmen to their new academic environment at the beginning of each semester. Offering additional organizational and technical guidance for their first year in university."
},
"ra_ude": {
"title": "Research Assistant @ UDE",
"time": "2021 - now",
"description": "While working for the Network Communication System Research Group at the University Duisburg-Essen as a research assistant I've assisted in research around software defined networking, 5G, congestion control algorithms and federated machine learning. I've established and managed the research groups on-premise and cloud infractructure, inventory system and online presence."
}
},
"education": {
"title": "Education",
"bachelor": {
"title": "BSc Systems Engineering",
"time": "2015 - now",
"description": ""
}
},
"skills": {
"title": "Skills",
"devops": {
"title": "DevOps",
"ansible": "Ansible",
"kubernetes": "Kubernetes",
"gitops": "GitOps"
},
"software_engineering": {
"title": "Software Engineering",
"rust": "Rust",
"python": "Python",
"csharp": "C#"
}
},
"languages": {
"title": "Languages",
"german": "German (Native)",
"english": "English (C2)",
"vietnamese": "Vietnamese (B1)",
"japanese": "Japanese (A1)"
},
"interests": {
"title": "Interests",
"coffee": "Coffee",
"tech_it": "Tech/IT",
"guitar": "Guitar",
"mechanical_keyboards": "Mechanical Keyboards"
}
},
"publications_projects": {
"publications": {
"title": "Publications",
"rpm": {
"title": "RPM: Reverse Path Congestion Marking on P4 Programmable Switches",
"authors": "N. Baganal-Krishna, T.-D. Tran, R. Kundel and A. Rizk",
"conference": "IEEE LCN 2023",
"url": "https://doi.org/10.48550/arXiv.2307.09639",
"description": "In this paper, we present Reverse Path Congestion Marking (RPM) to accelerate the reaction to network congestion events without changing the end-host stack. RPM decouples the congestion signal from the downstream path after the bottleneck while maintaining the stability of the congestion control loop. We show that RPM improves throughput fairness for RTT on heterogeneous TCP flows as well as the flow completion time, especially for small Data Center TCP (DCTCP) flows around P4 programmable ASIC switches."
},
"iot_fuzzers": {
"title": "Overview of IoT Fuzzing Techniques",
"authors": "Tuan-Dat Tran",
"conference": "Seminar",
"url": "https://git.tudattr.dev/AISE/seminar/src/branch/main/paper.pdf",
"description": "In this paper, we are comparing techniques used by IoT fuzzers to circumvent the challenges presented by IoT devices and the constraints of the solutions proposed by the IoT fuzzers."
}
},
"projects": {
"title": "Projects",
"bachelorproject": {
"title": "Undisclosed Ethereum Smart Contract Fuzzer",
"authors": "Tuan-Dat Tran",
"kind": "Bachelorproject",
"url": "https://git.ude-syssec.de/uni-due-syssec/students/2022_tuan-dat_tran_libAFLEVMFuzzer/ethfuzz/",
"description": "In this ongoing project I am building an Ethereum Smart Contract Fuzzer. More info will follow."
},
"dotfiles": {
"title": ".dotfiles",
"authors": "Tuan-Dat Tran",
"kind": "Personal",
"url": "https://git.tudattr.dev/tudattr/dotfiles",
"description": "dotfiles is a slang term usually used for configuration files in Linux based systems. My dotfiles contain configurations for tools I frequently use as well as a documentation on how to set up my daily-use notebook. It provides a baseline for any personal Linux system I set up and allows for repeatability simplifies the process of setting up an ArchLinux based system. "
},
"homelab": {
"title": "Homelab",
"authors": "Tuan-Dat Tran",
"kind": "Personal",
"url": "https://git.tudattr.dev/tudattr/ansible",
"description": "Ansible is a automation engine which allows for automatic provisioning, configuration management and application deployment. I use ansible to set up my homelab, which serves as a platform for me to try out and learn new technologies. "
}
}
},
"impressum": {
"off": "Show Impressum",
"on": "Impressum"
},
"components": {
"under_construction": "This page is currently under construction"
},
"footer": {
"year": "© 2024 ",
"name": "Tuan-Dat Tran",
"rights": ". All Rights Reserved.",
"contact": "Contact"
}
}
}

View File

@@ -1,24 +1,29 @@
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
use crate::components::H1;
pub fn Footer() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "container mx-auto pb-4",
class: "container mx-auto",
// ToolsUsed {},
footer {
class:"rounded-lg shadow bg-gray-800",
class:"bg-white rounded-lg shadow dark:bg-gray-800",
div {
class:"w-full mx-auto p-4 flex items-center justify-between",
span {
class:"text-sm sm:text-center text-gray-400",
{ t!("footer_year") },
a { href: "#", class: "hover:underline", { t!("footer_name") }},
{ t!("footer_rights") }
class:"text-sm text-gray-500 sm:text-center dark:text-gray-400",
{ translate!(i18, "footer.year") },
a { href: "#", class: "hover:underline", { translate!(i18, "footer.name") }},
{ translate!(i18, "footer.rights") }
}
ul {
class:"flex flex-wrap items-center mt-3 text-sm font-medium text-gray-400 sm:mt-0",
class:"flex flex-wrap items-center mt-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:mt-0",
li {
Link { to:"mailto:tuan-dat.tran@tudattr.dev", class:"hover:underline", { t!("footer_contact") } }
Link { to:"mailto:tuan-dat.tran@tudattr.dev", class:"hover:underline", { translate!(i18, "footer.contact") } }
},
}
}
@@ -27,6 +32,42 @@ pub fn Footer() -> Element {
}
}
#[allow(dead_code)]
fn ToolsUsed() -> Element {
rsx! {
div {
class:"items-center bg-white rounded-lg shadow dark:bg-gray-800 p-4 my-4",
div {
H1 {class: "justify-center", "Tools used" },
}
div {
class: "flex h-fill overflow-x-auto",
Logo {
src: "https://raw.githubusercontent.com/SAWARATSUKI/Logogs/main/Rust/Rust.png",
alt: "Rust"
},
Logo {
src: "https://raw.githubusercontent.com/SAWARATSUKI/Logogs/main/Tailwindcss/Tailwindcss6.png",
alt: "Tailwindcss"
},
Logo {
src: "https://raw.githubusercontent.com/SAWARATSUKI/Logogs/main/Html/HTML.png",
alt: "HTML"
},
Logo {
src: "https://raw.githubusercontent.com/Aikoyori/ProgrammingVTuberLogos/main/Docker/DockerLogo.png",
alt: "Docker"
},
Logo {
src: "https://raw.githubusercontent.com/Aikoyori/ProgrammingVTuberLogos/main/Neovim/NeovimLogo.png",
alt: "NeoVim"
},
}
}
}
}
#[component]
fn Logo(src: String, alt: String) -> Element {
rsx! {

View File

@@ -1,27 +1,31 @@
use dioxus::prelude::*;
use dioxus_i18n::{prelude::i18n, t, unic_langid::langid};
use dioxus_sdk::{i18n::*, translate};
use crate::Route;
pub fn Header() -> Element {
let i18 = use_i18();
rsx! {
nav {
div {
class: "container mx-auto py-4",
// class: "justify-between p-4 space-x-8",
class: "container mx-auto p-4",
ul {
class:"flex flex-col justify-between items-center justify-center space-y-2 sm:flex-row sm:space-y-0 sm:space-s-4",
class:"flex flex-col justify-between sm:flex-row justify-center space-y-2 sm:space-y-0 sm:space-s-4",
li {
Link {
to: Route::Home {},
class: "rounded-md shadow-sm",
img { src:asset!("./assets/pictures/ClackCat_t.webp"), class:"rounded-full h-8", alt:"TuDatTr Logo" },
img { src:"/pictures/ClackCat_t.webp", class:"rounded-full h-8", alt:"TuDatTr Logo" },
},
},
li { HeaderLink { url: Route::Home {}, text: t!("headers_home")} },
li { HeaderLink { url: Route::CV {}, text: t!("headers_cv") } },
li { HeaderLink { url: Route::PublicationsProjects {}, text: t!("headers_publications_projects") } },
li { HeaderLink { url: Route::Impressum {}, text: t!("headers_about") } },
li { HeaderLink { url: Route::Home {}, text: translate!(i18, "headers.home")} },
li { HeaderLink { url: Route::CV {}, text: translate!(i18, "headers.cv") } },
li { HeaderLink { url: Route::PublicationsProjects {}, text: translate!(i18, "headers.publications_projects") } },
li { HeaderLink { url: Route::Consulting {}, text: translate!(i18, "headers.consulting") } },
li { HeaderLink { url: Route::Impressum {}, text: translate!(i18, "headers.about") } },
li { LanguageButtonGroup {} },
},
}
@@ -31,22 +35,16 @@ pub fn Header() -> Element {
#[component]
fn LanguageButtonGroup() -> Element {
let mut i18n = i18n();
let mut i18 = use_i18();
let change_to_english = move |_| i18n.set_language(langid!("en-GB"));
let change_to_german = move |_| i18n.set_language(langid!("de-DE"));
let change_to_english = move |_| i18.set_language("en-GB".parse().unwrap());
let change_to_german = move |_| i18.set_language("de-DE".parse().unwrap());
rsx! {
div {
class: "rounded-md shadow-sm justify-end",
button {
class: "px-4 py-2 text-sm font-medium border rounded-s-lg focus:z-10 focus:ring-2 bg-gray-800 border-gray-700 text-white hover:text-white hover:bg-gray-700 focus:ring-blue-500 focus:text-white",
onclick: change_to_english,
label { { t!("headers_language_buttons_english") } } },
button {
class: "px-4 py-2 text-sm font-medium border rounded-e-lg focus:z-10 focus:ring-2 bg-gray-800 border-gray-700 text-white hover:text-white hover:bg-gray-700 focus:ring-blue-500 focus:text-white",
onclick: change_to_german,
label { { t!("headers_language_buttons_german") } } }
class: "rounded-md shadow-sm justify-end",
button { class: "px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white", onclick: change_to_english, label { { translate!(i18, "headers.language_buttons.english") } } },
button { class: "px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white", onclick: change_to_german, label { { translate!(i18, "headers.language_buttons.german") } } }
}
}
}
@@ -54,6 +52,6 @@ fn LanguageButtonGroup() -> Element {
#[component]
fn HeaderLink(url: Route, text: String) -> Element {
rsx! {
Link { to: url, class:"md:bg-transparent md:p-0 text-blue-500", {text} }
Link { to: url, class:"md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500", {text} }
}
}

View File

@@ -10,12 +10,12 @@ use header::Header;
pub fn Layout() -> Element {
rsx! {
div {
class: "dark flex flex-col min-h-screen",
class: "flex flex-col min-h-screen",
Header {},
Body {
Outlet::<Route> {},
},
Footer {}
Footer {},
}
}
}
}

View File

@@ -1,27 +1,30 @@
#![allow(non_snake_case)]
use std::str::FromStr;
use components::H1;
use dioxus::prelude::*;
use dioxus_sdk::i18n::*;
use dioxus_i18n::prelude::use_init_i18n;
use dioxus_i18n::prelude::I18nConfig;
use dioxus_i18n::prelude::Locale;
use dioxus_i18n::unic_langid::langid;
use layout::footer::Footer;
use layout::header::Header;
use tracing::{Level, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tracing::Level;
pub mod components;
mod consulting;
mod cv;
mod home;
mod impressum;
mod languages;
mod layout;
mod publications;
use crate::consulting::Consulting;
use crate::cv::CV;
use crate::home::Home;
use crate::impressum::Impressum;
use crate::languages::DE_DE;
use crate::languages::EN_GB;
use crate::layout::Layout;
use crate::publications::PublicationsProjects;
@@ -36,45 +39,30 @@ pub enum Route {
PublicationsProjects {},
#[route("/resume")]
CV {},
#[route("/health")]
Health {},
#[route("/consulting")]
Consulting {},
#[end_layout]
#[route("/:..route")]
PageNotFound { route: Vec<String> },
}
fn main() {
// Configure tracing to output JSON logs
tracing_subscriber::registry()
.with(EnvFilter::from_default_env().add_directive(Level::INFO.into()))
.with(fmt::layer().json())
.init();
info!("Starting Dioxus application...");
LaunchBuilder::new().launch(App);
}
#[component]
fn Health() -> Element {
rsx! { "OK" }
dioxus_logger::init(Level::DEBUG).expect("failed to init logger");
LaunchBuilder::fullstack()
.with_cfg(server_only!(dioxus::fullstack::Config::new().addr(
std::net::SocketAddrV4::new(std::net::Ipv4Addr::new(0, 0, 0, 0), 8080,)
)))
.launch(App)
}
fn App() -> Element {
use_init_i18n(|| {
I18nConfig::new(langid!("en-GB"))
.with_locale(Locale::new_static(
langid!("en-GB"),
include_str!("../languages/en-GB.ftl"),
))
.with_locale(Locale::new_static(
langid!("de-DE"),
include_str!("../languages/de-DE.ftl"),
))
use_init_i18n("en-GB".parse().unwrap(), "en-GB".parse().unwrap(), || {
let en_gb = Language::from_str(EN_GB).unwrap();
let de_de = Language::from_str(DE_DE).unwrap();
vec![en_gb, de_de]
});
rsx! {
document::Link { rel: "stylesheet", href: asset!("./assets/tailwind.css") }
document::Link { rel: "icon", href: asset!("./assets/favicon.ico") }
meta {
name: "description",
content: "Visit Tuan-Dat Tran's website for his CV, publications, projects, and consulting services. Connect for collaboration.",
@@ -94,8 +82,24 @@ fn App() -> Element {
}}
"
}
// meta {
// property: "og:title",
// content: "Tuan-Dat Trans Personal Website",
// }
// meta {
// property: "og:description",
// content: "Explore Tuan-Dat Tran's personal website featuring his CV, publications, projects, and consulting services. Get insights into his professional journey and connect for collaboration opportunities.",
// }
// meta {
// property: "og:image",
// content: "https://www.tudattr.dev/pictures/headshot.webp",
// }
// meta {
// property: "og:url",
// content: "https://tudattr.dev",
// }
div {
class: "bg-gray-900 min-h-screen",
class: "bg-white dark:bg-gray-900 min-h-screen",
Router::<Route> {},
}
}

View File

@@ -1,129 +1,177 @@
use dioxus::prelude::*;
use dioxus_i18n::t;
use dioxus_sdk::{i18n::use_i18, translate};
use crate::components::{BadgeList, Bolding, H1, HR};
use crate::components::{UnderConstruction, H1, HR};
#[component]
pub fn PublicationsProjects() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "flex flex-col ",
UnderConstruction { },
div {
H1 { { t!("publications_projects_publications_title") } }
H1 { { translate!(i18, "publications_projects.publications.title") } }
Publications { },
},
HR {},
div {
H1 { { t!("publications_projects_projects_title") } }
H1 { { translate!(i18, "publications_projects.projects.title") } }
Projects { },
}
}
}
}
#[derive(Clone, PartialEq, Props)]
struct PublicationProp {
#[props(default = "".to_string())]
doi: String,
authors: String,
title: String,
conference: String,
#[props(default = "".to_string())]
description: String,
}
fn Publications() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "flex gap-4 items-center flex-wrap",
Publication {
title: translate!(i18, "publications_projects.publications.rpm.title"),
authors: translate!(i18, "publications_projects.publications.rpm.authors"),
conference: translate!(i18, "publications_projects.publications.rpm.conference"),
doi: translate!(i18, "publications_projects.publications.rpm.url"),
description: translate!(i18, "publications_projects.publications.rpm.description")
},
Publication {
title: translate!(i18, "publications_projects.publications.iot_fuzzers.title"),
authors: translate!(i18, "publications_projects.publications.iot_fuzzers.authors"),
conference: translate!(i18, "publications_projects.publications.iot_fuzzers.conference"),
doi: translate!(i18, "publications_projects.publications.iot_fuzzers.url"),
description: translate!(i18, "publications_projects.publications.iot_fuzzers.description")
},
}
}
}
fn Publication(prop: PublicationProp) -> Element {
let pattern = "T.-D. Tran";
rsx! {
Link {
class:"block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700",
to:"{prop.doi}",
new_tab: true,
h5 {
class:"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white",
"{prop.title}",
},
span { class: "text-lg text-gray-900 dark:text-white", "{prop.conference}" },
p {
class:"font-normal text-gray-700 dark:text-gray-400 italic",
Authors {
authors: "{prop.authors}",
pattern: "{pattern}",
},
}
p {
class:"font-normal text-gray-700 dark:text-gray-400",
"{prop.description}",
}
}
}
}
fn Projects() -> Element {
let i18 = use_i18();
rsx! {
div {
class: "flex gap-4 items-center flex-wrap",
Project {
title: translate!(i18, "publications_projects.projects.bachelorproject.title"),
authors: translate!(i18, "publications_projects.projects.bachelorproject.authors"),
kind: translate!(i18, "publications_projects.projects.bachelorproject.kind"),
url: translate!(i18, "publications_projects.projects.bachelorproject.url"),
description: translate!(i18, "publications_projects.projects.bachelorproject.description")
},
Project {
title: translate!(i18, "publications_projects.projects.dotfiles.title"),
authors: translate!(i18, "publications_projects.projects.dotfiles.authors"),
kind: translate!(i18, "publications_projects.projects.dotfiles.kind"),
url: translate!(i18, "publications_projects.projects.dotfiles.url"),
description: translate!(i18, "publications_projects.projects.dotfiles.description")
},
Project {
title: translate!(i18, "publications_projects.projects.homelab.title"),
authors: translate!(i18, "publications_projects.projects.homelab.authors"),
kind: translate!(i18, "publications_projects.projects.homelab.kind"),
url: translate!(i18, "publications_projects.projects.homelab.url"),
description: translate!(i18, "publications_projects.projects.homelab.description")
}
}
}
}
#[derive(Clone, PartialEq, Props)]
struct ProjectProp {
#[props(default = "".to_string())]
url: String,
authors: String,
title: String,
technologies: Vec<String>,
kind: String,
#[props(default = "".to_string())]
description: String,
}
fn Project(prop: ProjectProp) -> Element {
let pattern = vec!["T.-D. Tran".to_string(), "Tuan-Dat Tran".to_string()];
let pattern = "T.-D. Tran";
rsx! {
Link {
class:"block max-w-sm p-6 border rounded-lg shadow bg-gray-800 border-gray-700 hover:bg-gray-700",
class:"block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700",
to:"{prop.url}",
new_tab: true,
h5 {
class:"mb-2 text-2xl font-bold tracking-tight text-white",
class:"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white",
"{prop.title}",
},
p {}
p { class: "text-lg text-white", "{prop.kind}" },
p { class: "text-lg text-gray-900 dark:text-white", "{prop.kind}" },
p {
class:"font-normal text-gray-400",
Bolding {
class:"font-normal text-gray-700 dark:text-gray-400",
Authors {
authors: "{prop.authors}",
patterns: pattern,
pattern: "{pattern}",
},
},
BadgeList{list: prop.technologies},
}
p {
class:"font-normal text-gray-400",
class:"font-normal text-gray-700 dark:text-gray-400",
"{prop.description}",
}
}
}
}
fn Publications() -> Element {
rsx! {
div {
class: "flex gap-4 items-center flex-wrap",
Project {
title: t!("publications_projects_publications_rpm_title"),
authors: t!("publications_projects_publications_rpm_authors"),
technologies: vec![],
kind: t!("publications_projects_publications_rpm_conference"),
url: t!("publications_projects_publications_rpm_url"),
description: t!("publications_projects_publications_rpm_description")
},
Project {
title: t!("publications_projects_publications_iot_fuzzers_title"),
authors: t!("publications_projects_publications_iot_fuzzers_authors"),
technologies: vec![],
kind: t!("publications_projects_publications_iot_fuzzers_conference"),
url: "/#",
description: t!("publications_projects_publications_iot_fuzzers_description")
},
}
}
#[derive(Clone, PartialEq, Props)]
struct AuthorProp {
authors: String,
pattern: String,
}
fn Projects() -> Element {
rsx! {
div {
class: "flex gap-4 items-center flex-wrap",
Project {
title: t!("publications_projects_projects_bpba_title"),
authors: t!("publications_projects_projects_bpba_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_bpba_kind"),
url: "/#",
description: t!("publications_projects_projects_bpba_description")
},
Project {
title: t!("publications_projects_projects_dotfiles_title"),
authors: t!("publications_projects_projects_dotfiles_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_dotfiles_kind"),
url: "/#",
description: t!("publications_projects_projects_dotfiles_description")
},
Project {
title: t!("publications_projects_projects_homelab_title"),
authors: t!("publications_projects_projects_homelab_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_homelab_kind"),
url: "/#",
description: t!("publications_projects_projects_homelab_description")
}
Project {
title: t!("publications_projects_projects_athome_title"),
authors: t!("publications_projects_projects_athome_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_athome_kind"),
url: "/#",
description: t!("publications_projects_projects_athome_description")
}
fn Authors(prop: AuthorProp) -> Element {
if let Some(start) = prop.authors.find(&prop.pattern) {
let end = start + prop.pattern.len();
let left = &prop.authors[..start];
let middle = &prop.authors[start..end];
let right = &prop.authors[end..];
rsx! {
"{left}" , b { "{middle}" }, "{right}",
}
} else {
rsx! { "{prop.authors}" }
}
}