9 Commits

Author SHA1 Message Date
Tuan-Dat Tran
fef5d771ba feat(dioxus): bump
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-11-04 01:00:50 +01:00
Tuan-Dat Tran
1284dd9dd6 chore: recompile
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-11-03 23:09:31 +01:00
Tuan-Dat Tran
538c99166f chore(deps): update rust dependencies
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-08-15 08:18:54 +02:00
Tuan-Dat Tran
6e3db0294f Add favorite tools
Some checks failed
Build Docker Image / Build (push) Has been cancelled
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-04-06 18:23:26 +02:00
Tuan-Dat Tran
a1fd3ea358 feat(badge): Streamlined badging
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-04-06 17:11:44 +02:00
Tuan-Dat Tran
a387293f94 feat(ftl): Updated description for dd
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-03-12 22:15:35 +01:00
Tuan-Dat Tran
64aa115ef4 feat(cargo): update
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-02-11 19:48:58 +01:00
Tuan-Dat Tran
8f00360ff8 feat(version): bumped to version 0.5.1
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-02-11 19:24:55 +01:00
Tuan-Dat Tran
16e04980b5 feat(dark-mode): removed dark mode, made it default
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2025-02-11 19:24:10 +01:00
20 changed files with 2540 additions and 2034 deletions

View File

@@ -1,18 +0,0 @@
name: Build Docker Image
on:
push:
branches:
- main
- dev
- cicd
jobs:
build:
name: Build
runs-on: [ubuntu-latest, aya01]
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
tags: tudattr/athome:latest

3674
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "athome" name = "athome"
version = "0.4.0" version = "0.2.0"
authors = ["Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>"] authors = ["Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>"]
edition = "2021" edition = "2021"
@@ -8,27 +8,17 @@ edition = "2021"
[dependencies] [dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
dioxus = { version = "0.6", features = ["fullstack", "router"] } dioxus = { version = "0.7.0", features = ["fullstack", "router"] }
# Debug # Debug
tracing = "0.1.40" tracing = "0.1.40"
dioxus-logger = "0.6.0" dioxus-logger = "0.7.0"
dioxus-i18n = "0.3.0" dioxus-i18n = { git = "https://github.com/Kannen/dioxus-i18n/", branch = "main"}
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
[features] [features]
default = [] default = ["web"]
web = ["dioxus/web"] web = ["dioxus/web"]
desktop = ["dioxus/desktop"] desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server"] 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,4 @@ title = "Tuan-Dat Tran"
reload_html = true reload_html = true
# which files or dirs will be watcher monitoring # which files or dirs will be watcher monitoring
watch_path = ["src", "assets"] watch_path = ["src", "assets", "languages"]

View File

@@ -19,7 +19,10 @@ ENV PATH="/.cargo/bin:$PATH"
# Create the final bundle folder. Bundle always executes in release mode with optimizations enabled # Create the final bundle folder. Bundle always executes in release mode with optimizations enabled
RUN dx bundle --platform web RUN dx bundle --platform web
FROM chef AS runtime 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 COPY --from=builder /app/target/dx/athome/release/web/ /usr/local/app
# set our port and make sure to listen for all connections # set our port and make sure to listen for all connections

View File

@@ -823,69 +823,69 @@ video {
border-inline-start-width: 1px; border-inline-start-width: 1px;
} }
.border-gray-200 { .border-gray-600 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(75 85 99 / var(--tw-border-opacity));
} }
.border-white { .border-gray-700 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity)); border-color: rgb(55 65 81 / var(--tw-border-opacity));
} }
.bg-blue-100 { .border-gray-900 {
--tw-bg-opacity: 1; --tw-border-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity)); border-color: rgb(17 24 39 / var(--tw-border-opacity));
} }
.bg-gray-100 { .bg-blue-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(30 58 138 / var(--tw-bg-opacity));
} }
.bg-gray-200 { .bg-gray-700 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity)); background-color: rgb(55 65 81 / var(--tw-bg-opacity));
} }
.bg-gray-50 { .bg-gray-800 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
} }
.bg-green-100 { .bg-gray-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity)); background-color: rgb(17 24 39 / var(--tw-bg-opacity));
} }
.bg-indigo-100 { .bg-green-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(224 231 255 / var(--tw-bg-opacity)); background-color: rgb(20 83 45 / var(--tw-bg-opacity));
} }
.bg-pink-100 { .bg-indigo-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(252 231 243 / var(--tw-bg-opacity)); background-color: rgb(49 46 129 / var(--tw-bg-opacity));
} }
.bg-purple-100 { .bg-pink-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity)); background-color: rgb(131 24 67 / var(--tw-bg-opacity));
} }
.bg-red-100 { .bg-purple-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity)); background-color: rgb(88 28 135 / var(--tw-bg-opacity));
} }
.bg-white { .bg-red-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(127 29 29 / var(--tw-bg-opacity));
} }
.bg-yellow-100 { .bg-yellow-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity)); background-color: rgb(113 63 18 / var(--tw-bg-opacity));
} }
.bg-gradient-to-br { .bg-gradient-to-br {
@@ -1047,10 +1047,6 @@ video {
text-transform: uppercase; text-transform: uppercase;
} }
.italic {
font-style: italic;
}
.leading-none { .leading-none {
line-height: 1; line-height: 1;
} }
@@ -1063,14 +1059,24 @@ video {
letter-spacing: 0.1em; letter-spacing: 0.1em;
} }
.text-blue-800 { .text-blue-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity)); color: rgb(147 197 253 / var(--tw-text-opacity));
} }
.text-cyan-600 { .text-blue-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(8 145 178 / var(--tw-text-opacity)); 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));
} }
.text-gray-400 { .text-gray-400 {
@@ -1083,44 +1089,34 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity)); 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 { .text-gray-900 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.text-green-800 { .text-green-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity)); color: rgb(134 239 172 / var(--tw-text-opacity));
} }
.text-indigo-800 { .text-indigo-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(55 48 163 / var(--tw-text-opacity)); color: rgb(165 180 252 / var(--tw-text-opacity));
} }
.text-pink-800 { .text-pink-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(157 23 77 / var(--tw-text-opacity)); color: rgb(249 168 212 / var(--tw-text-opacity));
} }
.text-purple-800 { .text-purple-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity)); color: rgb(216 180 254 / var(--tw-text-opacity));
} }
.text-red-800 { .text-red-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity)); color: rgb(252 165 165 / var(--tw-text-opacity));
} }
.text-transparent { .text-transparent {
@@ -1132,9 +1128,9 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.text-yellow-800 { .text-yellow-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity)); color: rgb(253 224 71 / var(--tw-text-opacity));
} }
.shadow { .shadow {
@@ -1174,20 +1170,17 @@ video {
transition-duration: 300ms; transition-duration: 300ms;
} }
.hover\:bg-gray-100:hover { @custom-variant dark (&:where(.dark, .dark *));
.hover\:bg-gray-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(55 65 81 / var(--tw-bg-opacity));
} }
.hover\:bg-gradient-to-br:hover { .hover\:bg-gradient-to-br:hover {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); 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 { .hover\:text-white:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
@@ -1206,9 +1199,9 @@ video {
z-index: 10; z-index: 10;
} }
.focus\:text-blue-700:focus { .focus\:text-white:focus {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.focus\:outline-none:focus { .focus\:outline-none:focus {
@@ -1228,14 +1221,14 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-blue-700:focus { .focus\:ring-blue-500:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(29 78 216 / var(--tw-ring-opacity)); --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
} }
.focus\:ring-cyan-300:focus { .focus\:ring-cyan-800:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(103 232 249 / var(--tw-ring-opacity)); --tw-ring-color: rgb(21 94 117 / var(--tw-ring-opacity));
} }
.group:hover .group-hover\:from-green-400 { .group:hover .group-hover\:from-green-400 {
@@ -1287,11 +1280,6 @@ video {
font-size: 3rem; font-size: 3rem;
line-height: 1; line-height: 1;
} }
.md\:text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -1301,163 +1289,3 @@ 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,7 +1,6 @@
headers_home = Home headers_home = Home
headers_cv = Lebenslauf headers_cv = Lebenslauf
headers_publications_projects = Artikel/Projekte headers_publications_projects = Artikel/Projekte
headers_consulting = Consulting
headers_about = Impressum headers_about = Impressum
headers_language_buttons_english = 🇬🇧 Englisch headers_language_buttons_english = 🇬🇧 Englisch
headers_language_buttons_german = 🇩🇪 Deutsch headers_language_buttons_german = 🇩🇪 Deutsch
@@ -12,13 +11,15 @@ home_card_text =
Willkommen auf meiner kleinen Webseite im World Wide Web. 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. Mein Name ist Tuan und ich bin Linux-Bastler, IT-Security Enthusiast und IT-Automatisierer aus Leidenschaft.
home_card_contact_button = Get in touch. home_card_contact_button = Get in touch.
cv_introduction_title = DevOps Engineer | Homelab Enthusiast
cv_introduction_0 = cv_introduction_0 =
DevOps-Engineer und Softwareentwickler mit starkem akademischen Hintergrund DevOps Engineer und Softwareentwickler mit starkem akademischen Hintergrund
in Netzwerktechnologien und Softwareentwicklung. Spezialisiert auf Kubernetes, in Netzwerktechnologien und Softwareentwicklung. Spezialisiert auf Kubernetes,
Ansible, Azure und moderne Cloud-Technologien. Praxisnahe Ansible, Azure und modernen Cloud-Technologien. Praxisnahe
Forschungserfahrung in Software-Defined Networking, 5G und Maschinellem Lernen. Forschungserfahrung in Software-Defined Networking, 5G und Maschinellem Lernen.
Leidenschaft für effiziente IT-Infrastrukturen, Automatisierung und Leidenschaft für effiziente IT-Infrastrukturen, Automatisierung und
innovative Softwarelösungen. innovative Softwarelösungen.
cv_introduction_tools = Lieblingstools:
cv_workexperience_title = Berufserfahrung cv_workexperience_title = Berufserfahrung
cv_workexperience_se1_gefeba_title = Software Entwickler @ gefeba Engineering GmbH cv_workexperience_se1_gefeba_title = Software Entwickler @ gefeba Engineering GmbH
cv_workexperience_se1_gefeba_time = 2013 - 2015 cv_workexperience_se1_gefeba_time = 2013 - 2015
@@ -60,19 +61,19 @@ cv_workexperience_ra_ude_description = Während meiner Tätigkeit bei der
aufgebaut und verwaltet. aufgebaut und verwaltet.
cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData
cv_workexperience_dd_devops_time = 2025 - Jetzt cv_workexperience_dd_devops_time = 2025 - Jetzt
cv_workexperience_dd_devops_description = Bei DextraData, einem führenden cv_workexperience_dd_devops_description = Als DevOps Engineer war ich
SaaS-Anbieter im Bereich Governance, Risk und Compliance (GRC), stelle ich verantwortlich für das Design, die Implementierung und die Wartung
die Verfügbarkeit und Stabilität von Software-Deployments für unsere Kunden skalierbarer Infrastrukturlösungen in verschiedenen SaaS-Produktumgebungen.
sowie für interne Entwicklungsteams über mehrere Produkte hinweg sicher. In Dies umfasste die Standardisierung und Optimierung von CI/CD-Pipelines sowie
Zusammenarbeit mit Customer-Success-Teams und Softwareentwicklern den Aufbau robuster Monitoring-Frameworks zur Gewährleistung hoher
gewährleisten wir einen reibungslosen Betrieb unserer Multi-Tenant- und Single Verfügbarkeit und Performance. Meine Arbeit konzentrierte sich auf einen
-Tenant-Instanzen von der Vorentwicklung bis hin zu den "Shift-Left"-Ansatz, der Entwicklungsteams durch Self-Service-Funktionen und
Produktionsumgebungen. optimierte operative Workflows befähigte, sowie auf die Verbesserung der
Systemzuverlässigkeit und -sicherheit.
cv_socials_title = Profile cv_socials_title = Profile
cv_education_title = Bildungsweg cv_education_title = Bildungsweg
cv_education_bachelor_title = BSc Angewandte Informatik - Systems Engineering cv_education_bachelor_title = BSc Angewandte Informatik - Systems Engineering
cv_education_bachelor_time = 2015 - jetzt cv_education_bachelor_time = 2015 - jetzt
cv_education_bachelor_description = ""
cv_skills_title = Fähigkeiten cv_skills_title = Fähigkeiten
cv_skills_devops_title = DevOps cv_skills_devops_title = DevOps
cv_skills_devops_ansible = Ansible cv_skills_devops_ansible = Ansible
@@ -108,7 +109,6 @@ publications_projects_publications_rpm_description = In diesem Artikel stellen
publications_projects_publications_iot_fuzzers_title = Overview of IoT Fuzzing Techniques 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_authors = Tuan-Dat Tran
publications_projects_publications_iot_fuzzers_conference = Seminar publications_projects_publications_iot_fuzzers_conference = Seminar
publications_projects_publications_iot_fuzzers_url = https://git.tudattr.dev/AISE/seminar/src/branch/main/paper.pdf
publications_projects_publications_iot_fuzzers_description = In dieser Arbeit publications_projects_publications_iot_fuzzers_description = In dieser Arbeit
vergleichen wir Methoden, die speziell von IoT Fuzzern genutzt werden um die vergleichen wir Methoden, die speziell von IoT Fuzzern genutzt werden um die
von IoT Geräten stammenden Herausforderungen und Einschränkungen zu umgehen. von IoT Geräten stammenden Herausforderungen und Einschränkungen zu umgehen.
@@ -116,13 +116,11 @@ publications_projects_projects_title = Projekte
publications_projects_projects_bpba_title = Unbenannter Ethereum Smart Contract Fuzzer publications_projects_projects_bpba_title = Unbenannter Ethereum Smart Contract Fuzzer
publications_projects_projects_bpba_authors = Tuan-Dat Tran publications_projects_projects_bpba_authors = Tuan-Dat Tran
publications_projects_projects_bpba_kind = Bachelorprojekt/Bachelorarbeit publications_projects_projects_bpba_kind = Bachelorprojekt/Bachelorarbeit
publications_projects_projects_bpba_url = https://git.ude-syssec.de/uni-due-syssec/students/2022_tuan-dat_tran_libAFLEVMFuzzer/ethfuzz/
publications_projects_projects_bpba_description = In diesem aktuell laufendem publications_projects_projects_bpba_description = In diesem aktuell laufendem
Projekt entwickle ich einen Ethereum Smart Contract Fuzzer. Mehr Infos folgen... Projekt entwickle ich einen Ethereum Smart Contract Fuzzer. Mehr Infos folgen...
publications_projects_projects_dotfiles_title = .dotfiles publications_projects_projects_dotfiles_title = .dotfiles
publications_projects_projects_dotfiles_authors = Tuan-Dat Tran publications_projects_projects_dotfiles_authors = Tuan-Dat Tran
publications_projects_projects_dotfiles_kind = Personal publications_projects_projects_dotfiles_kind = Personal
publications_projects_projects_dotfiles_url = https://git.tudattr.dev/tudattr/dotfiles
publications_projects_projects_dotfiles_description = dotfiles ist ein publications_projects_projects_dotfiles_description = dotfiles ist ein
umgangssprachlicher Begriff, der normalerweise für Konfigurationsdateien in umgangssprachlicher Begriff, der normalerweise für Konfigurationsdateien in
Linux-basierten Systemen verwendet wird. Meine Dotfiles enthalten Linux-basierten Systemen verwendet wird. Meine Dotfiles enthalten
@@ -134,7 +132,6 @@ publications_projects_projects_dotfiles_description = dotfiles ist ein
publications_projects_projects_homelab_title = Homelab publications_projects_projects_homelab_title = Homelab
publications_projects_projects_homelab_authors = Tuan-Dat Tran publications_projects_projects_homelab_authors = Tuan-Dat Tran
publications_projects_projects_homelab_kind = Personal publications_projects_projects_homelab_kind = Personal
publications_projects_projects_homelab_url = https://git.tudattr.dev/tudattr/ansible
publications_projects_projects_homelab_description = Ansible ist ein publications_projects_projects_homelab_description = Ansible ist ein
Automatisierungs-Werkzeug, die eine automatische Maschinenbereitstellung, Automatisierungs-Werkzeug, die eine automatische Maschinenbereitstellung,
Konfigurationsverwaltung und Anwendungsbereitstellung ermöglicht. Ich Konfigurationsverwaltung und Anwendungsbereitstellung ermöglicht. Ich
@@ -143,15 +140,14 @@ publications_projects_projects_homelab_description = Ansible ist ein
publications_projects_projects_athome_title = Diese Website publications_projects_projects_athome_title = Diese Website
publications_projects_projects_athome_authors = Tuan-Dat Tran publications_projects_projects_athome_authors = Tuan-Dat Tran
publications_projects_projects_athome_kind = Personal publications_projects_projects_athome_kind = Personal
publications_projects_projects_athome_url = /#
publications_projects_projects_athome_description = Diese Website ist eine mit publications_projects_projects_athome_description = Diese Website ist eine mit
dem auf Rust basiertem Dioxus Framwork und TailwindCSS gebaute Full Stack dem auf Rust basiertem Dioxus Framwork und TailwindCSS gebaute Full Stack
WASM Website, die sowohl zum Auffrischen von Web Themen, sowie als Rust WASM Website, die sowohl zum Auffrischen von Web Themen, sowie als Rust
Hobbyprojekt dient. Hobbyprojekt dient.
impressum_off = Impressum anzeigen
impressum_on = Impressum impressum_on = Impressum
component_under_construction = Diese Seite befindet sich gerade im Aufbau component_under_construction = Diese Seite befindet sich gerade im Aufbau
footer_year = © 2025 footer_year = © 2025
footer_name = Tuan-Dat Tran footer_name = Tuan-Dat Tran
footer_rights = . All Rights Reserved. footer_rights = . All Rights Reserved.
footer_contact = Kontakt footer_contact = Kontakt
link_opens_new_tab = (öffnet in neuem Tab)

View File

@@ -1,7 +1,6 @@
headers_home = Home headers_home = Home
headers_cv = Résumé headers_cv = Résumé
headers_publications_projects = Publications/Projects headers_publications_projects = Publications/Projects
headers_consulting = Consulting
headers_about = About headers_about = About
headers_language_buttons_english = 🇬🇧 English headers_language_buttons_english = 🇬🇧 English
headers_language_buttons_german = 🇩🇪 German headers_language_buttons_german = 🇩🇪 German
@@ -12,13 +11,16 @@ home_card_text =
Welcome to my little place on the internet 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. 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. home_card_contact_button = Get in touch.
cv_introduction_title = DevOps Engineer | Homelab Enthusiast
cv_introduction_0 = cv_introduction_0 =
A results-driven DevOps Engineer with a career in technology spanning over 8 years. My journey began with foundational part-time roles in software development (C#, Python) and academic research during my university studies. Now, I apply this deep technical understanding to my full-time DevOps role, where I specialize in building and maintaining scalable, high-availability SaaS infrastructure using Kubernetes, Azure, ArgoCD, and Ansible.
DevOps Engineer and Software Developer with a strong academic background in DevOps Engineer and Software Developer with a strong academic background in
networking technologies and software development. Specialized in Kubernetes, networking technologies and software development. Specialized in Kubernetes,
Ansible, Azure, and modern cloud technologies. Hands-on research experience Ansible, Azure, and modern cloud technologies. Hands-on research experience
in Software-Defined Networking, 5G, and Machine Learning. Passionate about in Software-Defined Networking, 5G, and Machine Learning. Passionate about
efficient IT infrastructures, automation, and innovative software efficient IT infrastructures, automation, and innovative software
solutions. solutions.
cv_introduction_tools = Favorite Tools:
cv_workexperience_title = Work Experience cv_workexperience_title = Work Experience
cv_workexperience_se1_gefeba_title = Software Engineer @ gefeba Engineering GmbH cv_workexperience_se1_gefeba_title = Software Engineer @ gefeba Engineering GmbH
cv_workexperience_se1_gefeba_time = 2013 - 2015 cv_workexperience_se1_gefeba_time = 2013 - 2015
@@ -56,18 +58,18 @@ cv_workexperience_ra_ude_description = While working at the Network
infractructure, inventory system and online presence. infractructure, inventory system and online presence.
cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData cv_workexperience_dd_devops_title = DevOps Engineer @ DextraData
cv_workexperience_dd_devops_time = 2025 - now cv_workexperience_dd_devops_time = 2025 - now
cv_workexperience_dd_devops_description = At DextraData, a leading SaaS cv_workexperience_dd_devops_description = As a DevOps Engineer, I was
provider in the Governance, Risk, and Compliance (GRC) sector, I ensure the responsible for designing, implementing, and maintaining scalable
availability and health of software deployments for our customers and infrastructure solutions across multiple SaaS product environments. This
internal development teams across multiple products. Collaborating with involved standardizing and optimizing CI/CD pipelines and establishing
Customer Success and Software Engineers alike we ensure smooth operations on robust monitoring frameworks to ensure high availability and performance.
our multi-tenant as well as single tenant instances from pre-dev to My work focused on a "shift-left" approach, empowering development teams
production environments. with self-service capabilities and streamlined operational workflows as
well as enhancement of system reliability and security.
cv_socials_title = Socials cv_socials_title = Socials
cv_education_title = Education cv_education_title = Education
cv_education_bachelor_title = BSc Systems Engineering cv_education_bachelor_title = BSc Systems Engineering
cv_education_bachelor_time = 2015 - now cv_education_bachelor_time = 2015 - now
cv_education_bachelor_description = ""
cv_skills_title = Skills cv_skills_title = Skills
cv_skills_devops_title = DevOps cv_skills_devops_title = DevOps
cv_skills_devops_ansible = Ansible cv_skills_devops_ansible = Ansible
@@ -103,7 +105,6 @@ publications_projects_publications_rpm_description = In this paper, we present
publications_projects_publications_iot_fuzzers_title = Overview of IoT Fuzzing Techniques 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_authors = Tuan-Dat Tran
publications_projects_publications_iot_fuzzers_conference = Seminar publications_projects_publications_iot_fuzzers_conference = Seminar
publications_projects_publications_iot_fuzzers_url = https://git.tudattr.dev/AISE/seminar/src/branch/main/paper.pdf
publications_projects_publications_iot_fuzzers_description = In this paper, we publications_projects_publications_iot_fuzzers_description = In this paper, we
are comparing techniques used by IoT fuzzers to circumvent the challenges are comparing techniques used by IoT fuzzers to circumvent the challenges
presented by IoT devices and the constraints of the solutions proposed by the presented by IoT devices and the constraints of the solutions proposed by the
@@ -112,12 +113,10 @@ publications_projects_projects_title = Projects
publications_projects_projects_bpba_title = Undisclosed Ethereum Smart Contract Fuzzer publications_projects_projects_bpba_title = Undisclosed Ethereum Smart Contract Fuzzer
publications_projects_projects_bpba_authors = Tuan-Dat Tran publications_projects_projects_bpba_authors = Tuan-Dat Tran
publications_projects_projects_bpba_kind = Bachelor Project/Bachelor Thesis publications_projects_projects_bpba_kind = Bachelor Project/Bachelor Thesis
publications_projects_projects_bpba_url = https://git.ude-syssec.de/uni-due-syssec/students/2022_tuan-dat_tran_libAFLEVMFuzzer/ethfuzz/
publications_projects_projects_bpba_description = In this ongoing project I am building an Ethereum Smart Contract Fuzzer. More info will follow. 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_title = .dotfiles
publications_projects_projects_dotfiles_authors = Tuan-Dat Tran publications_projects_projects_dotfiles_authors = Tuan-Dat Tran
publications_projects_projects_dotfiles_kind = Personal publications_projects_projects_dotfiles_kind = Personal
publications_projects_projects_dotfiles_url = https://git.tudattr.dev/tudattr/dotfiles
publications_projects_projects_dotfiles_description = dotfiles is a slang term publications_projects_projects_dotfiles_description = dotfiles is a slang term
usually used for configuration files in Linux based systems. My dotfiles usually used for configuration files in Linux based systems. My dotfiles
contain configurations for tools I frequently use as well as a documentation contain configurations for tools I frequently use as well as a documentation
@@ -127,7 +126,6 @@ publications_projects_projects_dotfiles_description = dotfiles is a slang term
publications_projects_projects_homelab_title = Homelab publications_projects_projects_homelab_title = Homelab
publications_projects_projects_homelab_authors = Tuan-Dat Tran publications_projects_projects_homelab_authors = Tuan-Dat Tran
publications_projects_projects_homelab_kind = Personal publications_projects_projects_homelab_kind = Personal
publications_projects_projects_homelab_url = https://git.tudattr.dev/tudattr/ansible
publications_projects_projects_homelab_description = Ansible is a automation publications_projects_projects_homelab_description = Ansible is a automation
tool which allows for automatic provisioning, configuration management and tool which allows for automatic provisioning, configuration management and
application deployment. I use ansible to set up my homelab, which serves as a application deployment. I use ansible to set up my homelab, which serves as a
@@ -135,15 +133,14 @@ publications_projects_projects_homelab_description = Ansible is a automation
publications_projects_projects_athome_title = This Website publications_projects_projects_athome_title = This Website
publications_projects_projects_athome_authors = Tuan-Dat Tran publications_projects_projects_athome_authors = Tuan-Dat Tran
publications_projects_projects_athome_kind = Personal publications_projects_projects_athome_kind = Personal
publications_projects_projects_athome_url = /#
publications_projects_projects_athome_description = This website is a publications_projects_projects_athome_description = This website is a
full-stack WASM site built using the Rust-based Dioxus framework and 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 TailwindCSS. It serves both as a way to refresh web development topics and as
a Rust hobby project. a Rust hobby project.
impressum_off = Show Impressum
impressum_on = Impressum impressum_on = Impressum
component_under_construction = This page is currently under construction component_under_construction = This page is currently under construction
footer_year = © 2025 footer_year = © 2025
footer_name = Tuan-Dat Tran footer_name = Tuan-Dat Tran
footer_rights = . All Rights Reserved. footer_rights = . All Rights Reserved.
footer_contact = Contact footer_contact = Contact
link_opens_new_tab = (opens in a new tab)

24
package-lock.json generated
View File

@@ -4,6 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": {
"caniuse-lite": "^1.0.30001753"
},
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.4.3" "tailwindcss": "^3.4.3"
} }
@@ -227,6 +230,26 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/caniuse-lite": {
"version": "1.0.30001753",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
"integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -782,6 +805,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",

View File

@@ -1,5 +1,8 @@
{ {
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.4.3" "tailwindcss": "^3.4.3"
},
"dependencies": {
"caniuse-lite": "^1.0.30001753"
} }
} }

3
rust-toolchain.toml Normal file
View File

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

61
scripts/check_i18n.py Normal file
View File

@@ -0,0 +1,61 @@
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

@@ -14,7 +14,7 @@ pub fn P(props: PProps) -> Element {
div { div {
class: "{props.class}", class: "{props.class}",
p { p {
class: "mb-2 font-normal text-gray-900 dark:text-white", class: "mb-2 font-normal text-white",
{props.children} {props.children}
}, },
} }
@@ -27,7 +27,7 @@ pub fn Title(props: PProps) -> Element {
div { div {
class: "{props.class}", class: "{props.class}",
h1 { h1 {
class: "mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white", class: "mb-2 text-2xl font-bold tracking-tight text-white",
{props.children} {props.children}
} }
} }
@@ -40,7 +40,7 @@ pub fn H1(props: PProps) -> Element {
div { div {
class: "{props.class}", class: "{props.class}",
h1 { h1 {
class: "mb-4 text-3xl font-extrabold text-gray-900 dark:text-white md:text-5xl lg:text-6xl", class: "mb-4 text-3xl font-extrabold text-white md:text-5xl lg:text-6xl",
span { span {
class: "text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400", class: "text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400",
{props.children} {props.children}
@@ -56,7 +56,7 @@ pub fn H4(props: PProps) -> Element {
class: "{props.class}", class: "{props.class}",
class: "mb-4", class: "mb-4",
h3 { h3 {
class: "text-lg uppercase text-cyan-600 dark:text-cyan-400 tracking-widest mb-4 font-bold", class: "text-lg uppercase text-cyan-400 tracking-widest mb-4 font-bold",
{props.children} {props.children}
} }
} }
@@ -69,7 +69,7 @@ pub fn H5(props: PProps) -> Element {
div { div {
class: "{props.class}", class: "{props.class}",
h5 { h5 {
class: "mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white", class: "mb-2 text-2xl font-bold tracking-tight text-white",
{props.children} {props.children}
} }
} }
@@ -89,7 +89,7 @@ pub struct CardProp {
pub fn Card(prop: CardProp) -> Element { pub fn Card(prop: CardProp) -> Element {
rsx! { rsx! {
div { 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-300 dark: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-800 rounded-lg px-5 py-2.5 min-w-fit max-w-8",
div { div {
class: "pb-4", class: "pb-4",
div { div {
@@ -97,7 +97,7 @@ pub fn Card(prop: CardProp) -> Element {
Picture {src: "{prop.picture}"}, Picture {src: "{prop.picture}"},
} }
} }
Title { "{prop.name}", span { class: "text-grey-600 dark:text-grey-500 text-lg", " {prop.gender}" } }, Title { "{prop.name}", span { class: "text-grey-500 text-lg", " {prop.gender}" } },
{ prop.children } { prop.children }
} }
} }
@@ -123,11 +123,11 @@ fn Picture(prop: PictureProp) -> Element {
pub fn UnderConstruction() -> Element { pub fn UnderConstruction() -> Element {
rsx! { rsx! {
div { div {
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", class:"rounded justify-between w-full p-4 border-b bg-gray-700 border-gray-600 my-8",
div { div {
class:"items-center mx-auto", class:"items-center mx-auto",
p { p {
class:"items-center text-sm font-normal text-gray-500 dark:text-gray-400", class:"items-center text-sm font-normal text-gray-400",
span { { t!("component_under_construction") } } span { { t!("component_under_construction") } }
} }
} }
@@ -137,7 +137,7 @@ pub fn UnderConstruction() -> Element {
pub fn HR() -> Element { pub fn HR() -> Element {
rsx! { rsx! {
hr { class:"h-px my-8 bg-gray-200 border-0 dark:bg-gray-700"} hr { class:"h-px my-8 border-0 bg-gray-700"}
} }
} }
@@ -242,3 +242,73 @@ pub fn Urling(prop: UrlingProp) -> Element {
} }
} }
} }
#[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}
}
}
}

View File

@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::t; use dioxus_i18n::t;
use crate::components::{H4, HR}; use crate::components::*;
#[component] #[component]
pub fn CV() -> Element { pub fn CV() -> Element {
@@ -13,7 +13,7 @@ pub fn CV() -> Element {
img { img {
class: "rounded-full w-24 h-24", class: "rounded-full w-24 h-24",
alt: "headshot", alt: "headshot",
src: asset!("./assets/pictures/headshot.webp") src: asset!("/assets/pictures/headshot.webp")
} }
Introduction {}, Introduction {},
Socials {} Socials {}
@@ -33,8 +33,18 @@ pub fn CV() -> Element {
fn Introduction() -> Element { fn Introduction() -> Element {
rsx! { rsx! {
div { div {
class: "flex", class: "flex-col",
h6 { class: "text-lg font-semibold text-white", { t!("cv_introduction_title") } },
P { { t!("cv_introduction_0") } }, P { { t!("cv_introduction_0") } },
P {
{ t!("cv_introduction_tools") },
" ",
AccessibleLink { new_tab: true, to: "https://www.lazyvim.org/", "NeoVim" },
", ",
AccessibleLink { new_tab: true, to: "https://zellij.dev/", "Zellij" },
", ",
AccessibleLink { new_tab: true, to: "https://k9scli.io/", "k9s" }
},
}, },
} }
} }
@@ -45,9 +55,9 @@ fn WorkExperience() -> Element {
class: "ms-8 max-w-3/4", class: "ms-8 max-w-3/4",
H4 { { t!("cv_workexperience_title") } }, H4 { { t!("cv_workexperience_title") } },
ol { ol {
class:"relative border-s border-gray-200 dark:border-gray-700", class:"relative border-s border-gray-700",
CVEntry {time: t!("cv_workexperience_dd_devops_time"), title: t!("cv_workexperience_dd_devops_title"), CVEntry {time: t!("cv_workexperience_dd_devops_time"), title: t!("cv_workexperience_dd_devops_title"),
technologies: vec!["Kubenertes".to_string(), "ArgoCD".to_string(), "Ansible".to_string(), "Azure".to_string(), "Docker".to_string(), "ELK".to_string()], technologies: vec!["Kubernetes".to_string(), "ArgoCD".to_string(), "Ansible".to_string(), "Azure".to_string(), "Elastic Stack".to_string(), "Helm".to_string()],
description: t!("cv_workexperience_dd_devops_description") description: t!("cv_workexperience_dd_devops_description")
}, },
CVEntry {time: t!("cv_workexperience_ra_ude_time"), title: t!("cv_workexperience_ra_ude_title"), CVEntry {time: t!("cv_workexperience_ra_ude_time"), title: t!("cv_workexperience_ra_ude_title"),
@@ -93,7 +103,7 @@ fn Education() -> Element {
H4 { { t!("cv_education_title") } }, H4 { { t!("cv_education_title") } },
Entry { Entry {
title: t!("cv_education_bachelor_title"), title: t!("cv_education_bachelor_title"),
time { class:"mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500", { t!("cv_education_bachelor_time") } }, time { class:"mb-1 text-sm font-normal leading-none text-gray-500", { t!("cv_education_bachelor_time") } },
}, },
} }
} }
@@ -160,7 +170,7 @@ fn Entry(props: EntryProps) -> Element {
rsx! { rsx! {
div { div {
class: "{props.class}", class: "{props.class}",
h6 { class: "font-semibold text-gray-900 dark:text-white", "{props.title}"} h6 { class: "font-semibold text-white", "{props.title}"}
{props.children}, {props.children},
} }
} }
@@ -183,47 +193,16 @@ fn CVEntry(props: CVEntryProps) -> Element {
li { li {
class: "max-w-xl", class: "max-w-xl",
class: "{props.class}", class: "{props.class}",
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"}, 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-400 dark:text-gray-500", "{props.time}"}, time { class:"mb-1 text-sm font-normal leading-none text-gray-500", "{props.time}"},
h6 { class: "text-lg font-semibold text-gray-900 dark:text-white", "{props.title}"} h6 { class: "text-lg font-semibold text-white", "{props.title}"}
ul { BadgeList{ list: props.technologies }
class: "flex flex-wrap gap-2", p { class:"text-base font-normal text-gray-400", "{props.description}"},
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} {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 {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 { fn Socials() -> Element {
rsx! { rsx! {
div { div {
@@ -231,7 +210,7 @@ fn Socials() -> Element {
H4 { { t!("cv_socials_title") } }, H4 { { t!("cv_socials_title") } },
div { div {
class: "flex justify-center items-center space-x-4", class: "flex justify-center items-center space-x-4",
P { Link { 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" } }}, 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" } }},
} }
} }
} }
@@ -241,7 +220,7 @@ fn Socials() -> Element {
fn P(children: Element) -> Element { fn P(children: Element) -> Element {
rsx! { rsx! {
p { p {
class: "text-base font-normal text-gray-500 dark:text-gray-400", class: "text-base font-normal text-gray-400",
{children}, {children},
} }
} }

View File

@@ -1,4 +1,4 @@
use crate::components::{Card, P}; use crate::components::{AccessibleLink, Card, P};
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::t; use dioxus_i18n::t;
@@ -10,7 +10,7 @@ pub fn Home() -> Element {
Card { Card {
name: t!("home_card_name"), name: t!("home_card_name"),
gender: t!("home_card_gender"), gender: t!("home_card_gender"),
picture: asset!("./assets/pictures/headshot.webp"), picture: asset!("/assets/pictures/headshot.webp"),
div { div {
class: "py-4", class: "py-4",
div { div {
@@ -20,9 +20,10 @@ pub fn Home() -> Element {
} }
}, },
}, },
Link { AccessibleLink {
to: "mailto:tuan-dat.tran@tudattr.dev", 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", 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") } { t!("home_card_contact_button") }
} }
}, },

View File

@@ -1,59 +1,30 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::t;
use crate::components::{H1, HR, P}; use crate::components::{H1, HR, P};
#[component] #[component]
pub fn Impressum() -> Element { pub fn Impressum() -> Element {
let mut impressum = use_signal(Vec::<String>::new);
let mut contact = use_signal(Vec::<String>::new);
rsx! { rsx! {
div { div {
div { div {
class: "flex flex-col items-center", class: "flex flex-col items-center",
button { H1 { "Impressum" },
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());
}
},
H1 { { t!("impressum_on") } },
},
}, },
HR{},
div { div {
class: "flex flex-col items-center", class: "flex flex-col items-center",
for line in impressum() { P { {"Tuan-Dat Tran"} },
P { {line} } P { {"c/o AutorenServices.de"} },
} P { {"Birkenallee 24"} },
P { {"36037 Fulda"} },
} }
if !impressum.read().is_empty() { HR{} }, HR{},
div { div {
class: "flex flex-col items-center", class: "flex flex-col items-center",
for line in contact() { P { {"tuan-dat.tran(at)tudattr(dot)dev"} },
P { {line} } P { {"+49 17(six) 83(four)683(eight)8"} },
}
} }
} }
} }
} }
#[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(),
])
}

View File

@@ -6,17 +6,19 @@ pub fn Footer() -> Element {
div { div {
class: "container mx-auto pb-4", class: "container mx-auto pb-4",
footer { footer {
class:"bg-white rounded-lg shadow dark:bg-gray-800", class:"rounded-lg shadow bg-gray-800",
div { div {
class:"w-full mx-auto p-4 flex items-center justify-between", class:"w-full mx-auto p-4 flex items-center justify-between",
span { span {
class:"text-sm text-gray-500 sm:text-center dark:text-gray-400", class:"text-sm sm:text-center text-gray-400",
{ t!("footer_year") }, { t!("footer_year") },
" ",
a { href: "#", class: "hover:underline", { t!("footer_name") }}, a { href: "#", class: "hover:underline", { t!("footer_name") }},
" ",
{ t!("footer_rights") } { t!("footer_rights") }
} }
ul { ul {
class:"flex flex-wrap items-center mt-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:mt-0", class:"flex flex-wrap items-center mt-3 text-sm font-medium text-gray-400 sm:mt-0",
li { 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", { t!("footer_contact") } }
}, },

View File

@@ -1,5 +1,5 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::{prelude::i18n, t, unic_langid::langid}; use dioxus_i18n::{prelude::*, t, unic_langid::langid};
use crate::Route; use crate::Route;
@@ -15,7 +15,7 @@ pub fn Header() -> Element {
Link { Link {
to: Route::Home {}, to: Route::Home {},
class: "rounded-md shadow-sm", class: "rounded-md shadow-sm",
img { src:asset!("./assets/pictures/ClackCat_t.webp"), class:"rounded-full h-8", alt:"TuDatTr Logo" }, img { src:asset!("/assets/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::Home {}, text: t!("headers_home")} },
@@ -40,11 +40,11 @@ fn LanguageButtonGroup() -> Element {
div { div {
class: "rounded-md shadow-sm justify-end", class: "rounded-md shadow-sm justify-end",
button { 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", 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, onclick: change_to_english,
label { { t!("headers_language_buttons_english") } } }, label { { t!("headers_language_buttons_english") } } },
button { 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", 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, onclick: change_to_german,
label { { t!("headers_language_buttons_german") } } } label { { t!("headers_language_buttons_german") } } }
} }
@@ -54,6 +54,6 @@ fn LanguageButtonGroup() -> Element {
#[component] #[component]
fn HeaderLink(url: Route, text: String) -> Element { fn HeaderLink(url: Route, text: String) -> Element {
rsx! { rsx! {
Link { to: url, class:"md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500", {text} } Link { to: url, class:"md:bg-transparent md:p-0 text-blue-500", {text} }
} }
} }

View File

@@ -3,13 +3,11 @@
use components::H1; use components::H1;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::prelude::use_init_i18n; use dioxus_i18n::{prelude::*, unic_langid::langid};
use dioxus_i18n::prelude::I18nConfig;
use dioxus_i18n::prelude::Locale;
use dioxus_i18n::unic_langid::langid;
use layout::footer::Footer; use layout::footer::Footer;
use layout::header::Header; use layout::header::Header;
use tracing::Level; use tracing::{Level, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
pub mod components; pub mod components;
mod cv; mod cv;
@@ -35,32 +33,42 @@ pub enum Route {
PublicationsProjects {}, PublicationsProjects {},
#[route("/resume")] #[route("/resume")]
CV {}, CV {},
#[route("/health")]
Health {},
#[end_layout] #[end_layout]
#[route("/:..route")] #[route("/:..route")]
PageNotFound { route: Vec<String> }, PageNotFound { route: Vec<String> },
} }
fn main() { fn main() {
dioxus_logger::init(Level::DEBUG).expect("failed to init logger"); // Configure tracing to output JSON logs
LaunchBuilder::new().launch(App) 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" }
} }
fn App() -> Element { fn App() -> Element {
use_init_i18n(|| { use_init_i18n(|| {
I18nConfig::new(langid!("en-GB")) I18nConfig::new(langid!("en-GB"))
.with_locale((langid!("de-DE"), include_str!("../languages/de-DE.ftl")))
.with_locale(Locale::new_static( .with_locale(Locale::new_static(
langid!("en-GB"), langid!("en-GB"),
include_str!("./languages/en-GB.ftl"), include_str!("../languages/en-GB.ftl"),
))
.with_locale(Locale::new_static(
langid!("de-DE"),
include_str!("./languages/de-DE.ftl"),
)) ))
}); });
rsx! { rsx! {
document::Link { rel: "stylesheet", href: asset!("./assets/tailwind.css") } document::Link { rel: "stylesheet", href: asset!("/assets/tailwind.css") }
document::Link { rel: "icon", href: asset!("./assets/favicon.ico") } document::Link { rel: "icon", href: asset!("/assets/favicon.ico") }
meta { meta {
name: "description", name: "description",
content: "Visit Tuan-Dat Tran's website for his CV, publications, projects, and consulting services. Connect for collaboration.", content: "Visit Tuan-Dat Tran's website for his CV, publications, projects, and consulting services. Connect for collaboration.",
@@ -81,7 +89,7 @@ fn App() -> Element {
" "
} }
div { div {
class: "bg-white dark:bg-gray-900 min-h-screen", class: "bg-gray-900 min-h-screen",
Router::<Route> {}, Router::<Route> {},
} }
} }

View File

@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_i18n::t; use dioxus_i18n::t;
use crate::components::{Bolding, H1, HR}; use crate::components::{BadgeList, Bolding, H1, HR};
#[component] #[component]
pub fn PublicationsProjects() -> Element { pub fn PublicationsProjects() -> Element {
@@ -22,64 +22,71 @@ pub fn PublicationsProjects() -> Element {
} }
#[derive(Clone, PartialEq, Props)] #[derive(Clone, PartialEq, Props)]
struct PublicationProp { struct ProjectProp {
#[props(default = "".to_string())] #[props(default = "".to_string())]
doi: String, url: String,
authors: String, authors: String,
title: String, title: String,
conference: String, technologies: Vec<String>,
kind: String,
#[props(default = "".to_string())] #[props(default = "".to_string())]
description: String, description: String,
} }
fn Project(prop: ProjectProp) -> Element {
let pattern = vec!["T.-D. Tran".to_string(), "Tuan-Dat Tran".to_string()];
rsx! {
Link {
class:"block max-w-sm p-6 border rounded-lg shadow bg-gray-800 border-gray-700 hover:bg-gray-700",
to:"{prop.url}",
new_tab: true,
h5 {
class:"mb-2 text-2xl font-bold tracking-tight text-white",
"{prop.title}",
},
p {}
p { class: "text-lg text-white", "{prop.kind}" },
p {
class:"font-normal text-gray-400",
Bolding {
authors: "{prop.authors}",
patterns: pattern,
},
},
BadgeList{list: prop.technologies},
p {
class:"font-normal text-gray-400",
"{prop.description}",
}
}
}
}
fn Publications() -> Element { fn Publications() -> Element {
rsx! { rsx! {
div { div {
class: "flex gap-4 items-center flex-wrap", class: "flex gap-4 items-center flex-wrap",
Publication { Project {
title: t!("publications_projects_publications_rpm_title"), title: t!("publications_projects_publications_rpm_title"),
authors: t!("publications_projects_publications_rpm_authors"), authors: t!("publications_projects_publications_rpm_authors"),
conference: t!("publications_projects_publications_rpm_conference"), technologies: vec![],
doi: t!("publications_projects_publications_rpm_url"), kind: t!("publications_projects_publications_rpm_conference"),
url: t!("publications_projects_publications_rpm_url"),
description: t!("publications_projects_publications_rpm_description") description: t!("publications_projects_publications_rpm_description")
}, },
Publication { Project {
title: t!("publications_projects_publications_iot_fuzzers_title"), title: t!("publications_projects_publications_iot_fuzzers_title"),
authors: t!("publications_projects_publications_iot_fuzzers_authors"), authors: t!("publications_projects_publications_iot_fuzzers_authors"),
conference: t!("publications_projects_publications_iot_fuzzers_conference"), technologies: vec![],
doi: "/#", kind: t!("publications_projects_publications_iot_fuzzers_conference"),
url: "/publications/#",
description: t!("publications_projects_publications_iot_fuzzers_description") description: t!("publications_projects_publications_iot_fuzzers_description")
}, },
} }
} }
} }
fn Publication(prop: PublicationProp) -> Element {
let pattern = vec!["T.-D. Tran".to_string(), "Tuan-Dat Tran".to_string()];
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",
Bolding {
authors: "{prop.authors}",
patterns: pattern,
},
}
p {
class:"font-normal text-gray-700 dark:text-gray-400",
"{prop.description}",
}
}
}
}
fn Projects() -> Element { fn Projects() -> Element {
rsx! { rsx! {
@@ -88,70 +95,35 @@ fn Projects() -> Element {
Project { Project {
title: t!("publications_projects_projects_bpba_title"), title: t!("publications_projects_projects_bpba_title"),
authors: t!("publications_projects_projects_bpba_authors"), authors: t!("publications_projects_projects_bpba_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_bpba_kind"), kind: t!("publications_projects_projects_bpba_kind"),
url: "/#", url: "/publications/#",
description: t!("publications_projects_projects_bpba_description") description: t!("publications_projects_projects_bpba_description")
}, },
Project { Project {
title: t!("publications_projects_projects_dotfiles_title"), title: t!("publications_projects_projects_dotfiles_title"),
authors: t!("publications_projects_projects_dotfiles_authors"), authors: t!("publications_projects_projects_dotfiles_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_dotfiles_kind"), kind: t!("publications_projects_projects_dotfiles_kind"),
url: "/#", url: "/publications/#",
description: t!("publications_projects_projects_dotfiles_description") description: t!("publications_projects_projects_dotfiles_description")
}, },
Project { Project {
title: t!("publications_projects_projects_homelab_title"), title: t!("publications_projects_projects_homelab_title"),
authors: t!("publications_projects_projects_homelab_authors"), authors: t!("publications_projects_projects_homelab_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_homelab_kind"), kind: t!("publications_projects_projects_homelab_kind"),
url: "/#", url: "/publications/#",
description: t!("publications_projects_projects_homelab_description") description: t!("publications_projects_projects_homelab_description")
} }
Project { Project {
title: t!("publications_projects_projects_athome_title"), title: t!("publications_projects_projects_athome_title"),
authors: t!("publications_projects_projects_athome_authors"), authors: t!("publications_projects_projects_athome_authors"),
technologies: vec![],
kind: t!("publications_projects_projects_athome_kind"), kind: t!("publications_projects_projects_athome_kind"),
url: "/#", url: "/publications/#",
description: t!("publications_projects_projects_athome_description") description: t!("publications_projects_projects_athome_description")
} }
} }
} }
} }
#[derive(Clone, PartialEq, Props)]
struct ProjectProp {
#[props(default = "".to_string())]
url: String,
authors: String,
title: 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()];
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.url}",
new_tab: true,
h5 {
class:"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white",
"{prop.title}",
},
p { class: "text-lg text-gray-900 dark:text-white", "{prop.kind}" },
p {
class:"font-normal text-gray-700 dark:text-gray-400",
Bolding {
authors: "{prop.authors}",
patterns: pattern,
},
}
p {
class:"font-normal text-gray-700 dark:text-gray-400",
"{prop.description}",
}
}
}
}