Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef5d771ba | ||
|
|
1284dd9dd6 | ||
|
|
538c99166f | ||
|
|
6e3db0294f | ||
|
|
a1fd3ea358 | ||
|
|
a387293f94 | ||
|
|
64aa115ef4 | ||
|
|
8f00360ff8 | ||
|
|
16e04980b5 |
@@ -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
3674
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -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"
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
@@ -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
24
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.91.0"
|
||||||
|
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||||
61
scripts/check_i18n.py
Normal file
61
scripts/check_i18n.py
Normal 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)
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
69
src/cv.rs
69
src/cv.rs
@@ -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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"} },
|
||||||
}
|
}
|
||||||
}
|
HR{},
|
||||||
if !impressum.read().is_empty() { 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(),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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") } }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/main.rs
38
src/main.rs
@@ -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> {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user