Toju Website V2
BIN
website/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
website/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
website/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
website/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 896 B |
BIN
website/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
website/public/iconsan.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
website/public/og-image.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
1
website/public/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
@@ -1,336 +1,8 @@
|
|||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
<app-particle-bg />
|
||||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
<div class="relative z-10 flex min-h-screen flex-col">
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
<app-header />
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
<main class="flex-1">
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
<router-outlet />
|
||||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
</main>
|
||||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
<app-footer />
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
|
||||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
|
||||||
--french-violet: oklch(47.66% 0.246 305.88);
|
|
||||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
|
||||||
--hot-red: oklch(61.42% 0.238 15.34);
|
|
||||||
--orange-red: oklch(63.32% 0.24 31.68);
|
|
||||||
|
|
||||||
--gray-900: oklch(19.37% 0.006 300.98);
|
|
||||||
--gray-700: oklch(36.98% 0.014 302.71);
|
|
||||||
--gray-400: oklch(70.9% 0.015 304.04);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
|
|
||||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.125rem;
|
|
||||||
color: var(--gray-900);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 100%;
|
|
||||||
letter-spacing: -0.125rem;
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--gray-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: inherit;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.angular-logo {
|
|
||||||
max-width: 9.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h1 {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content p {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
width: 1px;
|
|
||||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
|
||||||
margin-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
|
||||||
color: var(--pill-accent);
|
|
||||||
padding-inline: 0.75rem;
|
|
||||||
padding-block: 0.375rem;
|
|
||||||
border-radius: 2.75rem;
|
|
||||||
border: 0;
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
font-family: var(--inter-font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
letter-spacing: -0.00875rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill:hover {
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 1) {
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
}
|
|
||||||
.pill-group .pill:nth-child(6n + 2) {
|
|
||||||
--pill-accent: var(--french-violet);
|
|
||||||
}
|
|
||||||
.pill-group .pill:nth-child(6n + 3),
|
|
||||||
.pill-group .pill:nth-child(6n + 4),
|
|
||||||
.pill-group .pill:nth-child(6n + 5) {
|
|
||||||
--pill-accent: var(--hot-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group svg {
|
|
||||||
margin-inline-start: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.73rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links path {
|
|
||||||
transition: fill 0.3s ease;
|
|
||||||
fill: var(--gray-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links a:hover svg path {
|
|
||||||
fill: var(--gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
.content {
|
|
||||||
flex-direction: column;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
|
||||||
margin-block: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<main class="main">
|
|
||||||
<div class="content">
|
|
||||||
<div class="left-side">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 982 239"
|
|
||||||
fill="none"
|
|
||||||
class="angular-logo"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#a)">
|
|
||||||
<path
|
|
||||||
fill="url(#b)"
|
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="url(#c)"
|
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<radialGradient
|
|
||||||
id="c"
|
|
||||||
cx="0"
|
|
||||||
cy="0"
|
|
||||||
r="1"
|
|
||||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#FF41F8" />
|
|
||||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
|
|
||||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="b"
|
|
||||||
x1="0"
|
|
||||||
x2="982"
|
|
||||||
y1="192"
|
|
||||||
y2="192"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#F0060B" />
|
|
||||||
<stop offset="0" stop-color="#F0070C" />
|
|
||||||
<stop offset=".526" stop-color="#CC26D5" />
|
|
||||||
<stop offset="1" stop-color="#7702FF" />
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
<h1>Hello, {{ title }}</h1>
|
|
||||||
<p>Congratulations! Your app is running. 🎉</p>
|
|
||||||
</div>
|
|
||||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
|
||||||
<div class="right-side">
|
|
||||||
<div class="pill-group">
|
|
||||||
@for (item of [
|
|
||||||
{ title: 'Explore the Docs', link: 'https://angular.dev' },
|
|
||||||
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
|
|
||||||
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
|
|
||||||
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
|
|
||||||
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
|
|
||||||
]; track item.title) {
|
|
||||||
<a
|
|
||||||
class="pill"
|
|
||||||
[href]="item.link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
width="14"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a
|
|
||||||
href="https://github.com/angular/angular"
|
|
||||||
aria-label="Github"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="25"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 25 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Github"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/angular"
|
|
||||||
aria-label="Twitter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Twitter"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
|
||||||
aria-label="Youtube"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="29"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 29 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Youtube"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
|
|
||||||
|
|
||||||
<router-outlet />
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,18 +7,14 @@ import { ParticleBgComponent } from './components/particle-bg/particle-bg.compon
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, HeaderComponent, FooterComponent, ParticleBgComponent],
|
imports: [
|
||||||
template: `
|
RouterOutlet,
|
||||||
<app-particle-bg />
|
HeaderComponent,
|
||||||
<div class="relative z-10 flex flex-col min-h-screen">
|
FooterComponent,
|
||||||
<app-header />
|
ParticleBgComponent
|
||||||
<main class="flex-1">
|
],
|
||||||
<router-outlet />
|
templateUrl: './app.component.html',
|
||||||
</main>
|
styleUrl: './app.component.scss'
|
||||||
<app-footer />
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [`:host { display: block; }`],
|
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
|
||||||
import { provideServerRendering } from '@angular/platform-server';
|
import { provideServerRendering } from '@angular/platform-server';
|
||||||
import { appConfig } from './app.config';
|
import { appConfig } from './app.config';
|
||||||
|
|
||||||
const serverConfig: ApplicationConfig = {
|
const serverConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [provideServerRendering()]
|
||||||
provideServerRendering(),
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
withInMemoryScrolling({ scrollPositionRestoration: 'top', anchorScrolling: 'enabled' })
|
withInMemoryScrolling({ scrollPositionRestoration: 'top', anchorScrolling: 'enabled' })
|
||||||
),
|
),
|
||||||
provideClientHydration(withEventReplay()),
|
provideClientHydration(withEventReplay()),
|
||||||
provideHttpClient(withFetch()),
|
provideHttpClient(withFetch())
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,22 +3,36 @@ import { Routes } from '@angular/router';
|
|||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
|
loadComponent: () => import('./pages/home/home.component').then(
|
||||||
|
(homePageModule) => homePageModule.HomeComponent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'what-is-toju',
|
path: 'what-is-toju',
|
||||||
loadComponent: () => import('./pages/what-is-toju/what-is-toju.component').then(m => m.WhatIsTojuComponent),
|
loadComponent: () => import('./pages/what-is-toju/what-is-toju.component').then(
|
||||||
|
(whatIsTojuPageModule) => whatIsTojuPageModule.WhatIsTojuComponent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'downloads',
|
path: 'downloads',
|
||||||
loadComponent: () => import('./pages/downloads/downloads.component').then(m => m.DownloadsComponent),
|
loadComponent: () => import('./pages/downloads/downloads.component').then(
|
||||||
|
(downloadsPageModule) => downloadsPageModule.DownloadsComponent
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gallery',
|
||||||
|
loadComponent: () => import('./pages/gallery/gallery.component').then(
|
||||||
|
(galleryPageModule) => galleryPageModule.GalleryComponent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'philosophy',
|
path: 'philosophy',
|
||||||
loadComponent: () => import('./pages/philosophy/philosophy.component').then(m => m.PhilosophyComponent),
|
loadComponent: () => import('./pages/philosophy/philosophy.component').then(
|
||||||
|
(philosophyPageModule) => philosophyPageModule.PhilosophyComponent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: '',
|
redirectTo: ''
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@if (adService.adsEnabled()) {
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-dashed border-border/50 bg-card/30 min-h-[90px] flex items-center justify-center text-xs text-muted-foreground/50"
|
||||||
|
>
|
||||||
|
Advertisement
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -4,15 +4,7 @@ import { AdService } from '../../services/ad.service';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-ad-slot',
|
selector: 'app-ad-slot',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
templateUrl: './ad-slot.component.html'
|
||||||
@if (adService.adsEnabled()) {
|
|
||||||
<div class="container mx-auto px-6 py-4">
|
|
||||||
<div class="rounded-lg border border-dashed border-border/50 bg-card/30 min-h-[90px] flex items-center justify-center text-xs text-muted-foreground/50">
|
|
||||||
Advertisement
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class AdSlotComponent {
|
export class AdSlotComponent {
|
||||||
readonly adService = inject(AdService);
|
readonly adService = inject(AdService);
|
||||||
|
|||||||
171
website/src/app/components/footer/footer.component.html
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<footer class="border-t border-border/30 bg-background/80 backdrop-blur-sm">
|
||||||
|
<div class="container mx-auto px-6 py-16">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||||
|
<!-- Brand -->
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<img
|
||||||
|
src="/images/toju-logo-transparent.png"
|
||||||
|
alt="Toju"
|
||||||
|
class="h-8 w-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Free, open-source, peer-to-peer communication. Built by people who believe privacy is a right, not a premium feature.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Product</h4>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Downloads
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Web Version
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLink="/what-is-toju"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
What is Toju?
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLink="/gallery"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Image Gallery
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Community -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Community</h4>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://git.azaaxin.com/myxelium/Toju"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/gitea.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
Source Code
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/Myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/github.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/buymeacoffee.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
Support Us
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Values -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Values</h4>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLink="/philosophy"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Our Philosophy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><span class="text-sm text-muted-foreground">100% Free Forever</span></li>
|
||||||
|
<li><span class="text-sm text-muted-foreground">Open Source</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 pt-8 border-t border-border/30 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
<p class="text-xs text-muted-foreground">© {{ currentYear }} Myxelium. Toju is open-source software.</p>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://git.azaaxin.com/myxelium/Toju"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
aria-label="View source code on Gitea"
|
||||||
|
class="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/gitea.png"
|
||||||
|
alt=""
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
class="w-5 h-5 object-contain"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/Myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
aria-label="View the project on GitHub"
|
||||||
|
class="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/github.png"
|
||||||
|
alt=""
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
class="w-5 h-5 object-contain"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -5,65 +5,7 @@ import { RouterLink } from '@angular/router';
|
|||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink],
|
imports: [RouterLink],
|
||||||
template: `
|
templateUrl: './footer.component.html'
|
||||||
<footer class="border-t border-border/30 bg-background/80 backdrop-blur-sm">
|
|
||||||
<div class="container mx-auto px-6 py-16">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
|
|
||||||
<!-- Brand -->
|
|
||||||
<div class="md:col-span-1">
|
|
||||||
<div class="flex items-center gap-3 mb-4">
|
|
||||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-sm">T</div>
|
|
||||||
<span class="text-lg font-bold text-foreground">Toju</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
|
||||||
Free, open-source, peer-to-peer communication. Built by people who believe privacy is a right, not a premium feature.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product -->
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Product</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a routerLink="/downloads" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a></li>
|
|
||||||
<li><a href="https://web.toju.app/" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Web Version</a></li>
|
|
||||||
<li><a routerLink="/what-is-toju" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Community -->
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Community</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Source Code</a></li>
|
|
||||||
<li><a href="https://github.com/Myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">GitHub</a></li>
|
|
||||||
<li><a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Support Us</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Values -->
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Values</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a routerLink="/philosophy" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a></li>
|
|
||||||
<li><span class="text-sm text-muted-foreground">100% Free Forever</span></li>
|
|
||||||
<li><span class="text-sm text-muted-foreground">Open Source</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-12 pt-8 border-t border-border/30 flex flex-col md:flex-row justify-between items-center gap-4">
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
© {{ currentYear }} Myxelium. Toju is open-source software.
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
readonly currentYear = new Date().getFullYear();
|
readonly currentYear = new Date().getFullYear();
|
||||||
|
|||||||
184
website/src/app/components/header/header.component.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<header
|
||||||
|
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
||||||
|
[class]="scrolled() ? 'glass shadow-lg shadow-black/20' : 'bg-transparent'"
|
||||||
|
>
|
||||||
|
<nav class="container mx-auto px-6 py-4 flex items-center justify-between">
|
||||||
|
<!-- Logo -->
|
||||||
|
<a
|
||||||
|
routerLink="/"
|
||||||
|
aria-label="Toju home"
|
||||||
|
class="flex items-center group"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-1.5">
|
||||||
|
<img
|
||||||
|
src="/images/toju-logo-transparent.png"
|
||||||
|
alt="Toju"
|
||||||
|
class="h-9 w-auto object-contain drop-shadow-lg group-hover:opacity-90 transition-opacity"
|
||||||
|
/>
|
||||||
|
<span class="text-xl font-bold text-foreground">Toju</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="ml-2 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Beta
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Desktop nav -->
|
||||||
|
<div class="hidden md:flex items-center gap-8">
|
||||||
|
<a
|
||||||
|
routerLink="/"
|
||||||
|
routerLinkActive="text-primary"
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="/what-is-toju"
|
||||||
|
routerLinkActive="text-primary"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
What is Toju?
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
routerLinkActive="text-primary"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Downloads
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="/philosophy"
|
||||||
|
routerLinkActive="text-primary"
|
||||||
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Our Philosophy
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side -->
|
||||||
|
<div class="hidden md:flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm text-muted-foreground hover:text-yellow-400 transition-colors flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/buymeacoffee.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
Support Us
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||||
|
>
|
||||||
|
Use Web Version
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile hamburger -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md:hidden text-foreground p-2"
|
||||||
|
(click)="mobileOpen.set(!mobileOpen())"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
@if (mobileOpen()) {
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile nav -->
|
||||||
|
@if (mobileOpen()) {
|
||||||
|
<div
|
||||||
|
class="md:hidden glass border-t border-border/30 px-6 py-4 space-y-4"
|
||||||
|
(click)="mobileOpen.set(false)"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
routerLink="/"
|
||||||
|
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>Home</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
routerLink="/what-is-toju"
|
||||||
|
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>What is Toju?</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>Downloads</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
routerLink="/philosophy"
|
||||||
|
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>Our Philosophy</a
|
||||||
|
>
|
||||||
|
<hr class="border-border/30" />
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-yellow-400 transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/buymeacoffee.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
Support Us
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium"
|
||||||
|
>
|
||||||
|
Use Web Version
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</header>
|
||||||
@@ -12,87 +12,14 @@ import { isPlatformBrowser } from '@angular/common';
|
|||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink, RouterLinkActive],
|
imports: [RouterLink, RouterLinkActive],
|
||||||
template: `
|
templateUrl: './header.component.html'
|
||||||
<header
|
|
||||||
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
|
||||||
[class]="scrolled() ? 'glass shadow-lg shadow-black/20' : 'bg-transparent'"
|
|
||||||
>
|
|
||||||
<nav class="container mx-auto px-6 py-4 flex items-center justify-between">
|
|
||||||
<!-- Logo -->
|
|
||||||
<a routerLink="/" class="flex items-center gap-3 group">
|
|
||||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-lg shadow-lg shadow-purple-500/25 group-hover:shadow-purple-500/40 transition-shadow">
|
|
||||||
T
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-bold text-foreground">Toju</span>
|
|
||||||
<span class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider">Beta</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Desktop nav -->
|
|
||||||
<div class="hidden md:flex items-center gap-8">
|
|
||||||
<a routerLink="/" routerLinkActive="text-primary" [routerLinkActiveOptions]="{exact: true}" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
|
|
||||||
<a routerLink="/what-is-toju" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
|
|
||||||
<a routerLink="/downloads" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
|
|
||||||
<a routerLink="/philosophy" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right side -->
|
|
||||||
<div class="hidden md:flex items-center gap-4">
|
|
||||||
<a
|
|
||||||
href="https://buymeacoffee.com/myxelium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-sm text-muted-foreground hover:text-yellow-400 transition-colors flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.062 2.014.13l.04.005c.394.04.781.098 1.172.166.063.011.121.029.182.044.088.03.18.043.252.087a.556.556 0 01.272.476c.014.09.042.217.068.343.082.397.162.79.24 1.184.048.26.092.52.122.782.017.163.047.414-.076.556-.163.178-.49.132-.702.096a48.146 48.146 0 00-1.553-.22 30.38 30.38 0 00-3.346-.234c-1.108 0-2.217.066-3.315.2a25.855 25.855 0 00-1.578.244c-.156.03-.313.062-.469.097-.288.065-.564.195-.754.441-.131.17-.18.383-.134.596.046.213.167.396.34.525.294.215.626.358.976.454 1.015.285 2.087.38 3.136.397 1.21.018 2.42-.04 3.618-.177 1.004-.114 2-.3 2.95-.593.082-.025.165-.05.246-.078.224-.073.458-.142.676-.233.378-.157.727-.397.856-.795a1.38 1.38 0 00.048-.353v-.015c0-.049-.01-.096-.014-.144l-.018-.121c-.028-.179-.055-.362-.1-.539l-.025-.115-.038-.186c-.022-.11-.044-.22-.07-.327l-.061-.283c-.02-.096-.042-.19-.067-.284-.02-.074-.037-.15-.058-.224a2.98 2.98 0 00-.09-.3c-.034-.095-.078-.225-.148-.308zm-11.8 6.56a.612.612 0 00-.35.177.614.614 0 00-.17.343l-.14.697-.147.726c-.012.065-.018.13-.026.194l-.13.653c-.012.065-.018.13-.026.195l-.13.652c-.024.119-.042.24-.07.358-.018.073-.037.147-.05.222l-.087.437c-.024.12-.042.24-.07.359-.018.073-.037.146-.05.221l-.088.437c-.011.058-.022.117-.03.176-.01.06-.014.119-.025.178a.474.474 0 00.147.422c.105.1.24.153.378.154l1.06.005c.037 0 .073-.003.11-.009l.053-.009c.151-.03.293-.109.364-.255.043-.09.058-.186.065-.282v-.003l.05-.259.052-.259.023-.117c.01-.051.019-.103.03-.153l.005-.025.026-.132.027-.131.026-.131.052-.259.052-.259.013-.064.013-.064.024-.12.024-.121c.012-.062.007-.127-.003-.189a.57.57 0 00-.044-.15.538.538 0 00-.094-.14.596.596 0 00-.23-.155c-.07-.032-.147-.052-.226-.057-.118-.008-.236-.008-.355-.008h-.007c-.154 0-.308 0-.462.003-.107.002-.216.002-.323.006zm13.7-2.974a.53.53 0 01.128.378c-.004.064-.02.128-.048.186a.684.684 0 01-.097.148l-.012.014-.016.016a.585.585 0 01-.13.098.612.612 0 01-.21.075 1.378 1.378 0 01-.26.024h-.037c-.086 0-.132.093-.094.161a.3.3 0 01.032.063c.018.045.018.093.009.14-.011.059-.048.11-.09.154a.332.332 0 01-.145.1.375.375 0 01-.173.03.505.505 0 01-.152-.036.43.43 0 01-.123-.082.456.456 0 01-.085-.11.435.435 0 01-.044-.138 1.01 1.01 0 01-.017-.134.66.66 0 01.005-.133c.008-.053.032-.1.065-.143.035-.044.078-.08.126-.107a.498.498 0 01.163-.059c.05-.007.1-.007.15-.002h.015a.08.08 0 00.076-.048.083.083 0 00-.023-.096.568.568 0 01-.123-.15.516.516 0 01-.056-.185.582.582 0 01.013-.196.592.592 0 01.075-.176.538.538 0 01.198-.182.567.567 0 01.247-.07c.08-.003.16.011.232.044a.56.56 0 01.187.14z"/></svg>
|
|
||||||
Support Us
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://web.toju.app/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
|
||||||
>
|
|
||||||
Use Web Version
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile hamburger -->
|
|
||||||
<button
|
|
||||||
class="md:hidden text-foreground p-2"
|
|
||||||
(click)="mobileOpen.set(!mobileOpen())"
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
@if (mobileOpen()) {
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
||||||
} @else {
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile nav -->
|
|
||||||
@if (mobileOpen()) {
|
|
||||||
<div class="md:hidden glass border-t border-border/30 px-6 py-4 space-y-4" (click)="mobileOpen.set(false)">
|
|
||||||
<a routerLink="/" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
|
|
||||||
<a routerLink="/what-is-toju" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
|
|
||||||
<a routerLink="/downloads" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
|
|
||||||
<a routerLink="/philosophy" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
|
|
||||||
<hr class="border-border/30">
|
|
||||||
<a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener noreferrer" class="block text-sm text-muted-foreground hover:text-yellow-400 transition-colors">Support Us</a>
|
|
||||||
<a href="https://web.toju.app/" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium">Use Web Version</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</header>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent {
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
|
||||||
readonly scrolled = signal(false);
|
readonly scrolled = signal(false);
|
||||||
readonly mobileOpen = signal(false);
|
readonly mobileOpen = signal(false);
|
||||||
|
|
||||||
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
@HostListener('window:scroll')
|
@HostListener('window:scroll')
|
||||||
onScroll(): void {
|
onScroll(): void {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
|||||||
@@ -1,41 +1,53 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
OnInit,
|
|
||||||
OnDestroy,
|
|
||||||
inject,
|
|
||||||
PLATFORM_ID,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ViewChild
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
ViewChild,
|
||||||
|
inject
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-particle-bg',
|
selector: 'app-particle-bg',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: '<canvas #canvas class="fixed inset-0 pointer-events-none z-0" style="pointer-events: all;"></canvas>',
|
host: {
|
||||||
styles: [':host { display: block; position: fixed; inset: 0; z-index: 0; pointer-events: none; } canvas { pointer-events: all; }']
|
class: 'block fixed inset-0 z-0 pointer-events-none'
|
||||||
|
},
|
||||||
|
template: '<canvas #canvas class="absolute inset-0 h-full w-full pointer-events-auto"></canvas>'
|
||||||
})
|
})
|
||||||
export class ParticleBgComponent implements OnInit, OnDestroy {
|
export class ParticleBgComponent implements OnInit, OnDestroy {
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
@ViewChild('canvas', { static: true }) private canvasRef?: ElementRef<HTMLCanvasElement>;
|
||||||
@ViewChild('canvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
private ctx!: CanvasRenderingContext2D;
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
|
private context: CanvasRenderingContext2D | null = null;
|
||||||
private particles: Particle[] = [];
|
private particles: Particle[] = [];
|
||||||
private mouse = { x: -1000, y: -1000 };
|
private mousePosition = {
|
||||||
private animId = 0;
|
pointerX: -1000,
|
||||||
private resizeHandler = () => this.resize();
|
pointerY: -1000
|
||||||
private mouseMoveHandler = (e: MouseEvent) => {
|
|
||||||
this.mouse.x = e.clientX;
|
|
||||||
this.mouse.y = e.clientY;
|
|
||||||
};
|
};
|
||||||
|
private animationId = 0;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId))
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = this.canvasRef.nativeElement;
|
const canvas = this.canvasRef?.nativeElement;
|
||||||
|
|
||||||
this.ctx = canvas.getContext('2d')!;
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
window.addEventListener('resize', this.resizeHandler);
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
@@ -46,32 +58,46 @@ export class ParticleBgComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (!isPlatformBrowser(this.platformId))
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cancelAnimationFrame(this.animId);
|
cancelAnimationFrame(this.animationId);
|
||||||
window.removeEventListener('resize', this.resizeHandler);
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
window.removeEventListener('mousemove', this.mouseMoveHandler);
|
window.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly resizeHandler = () => this.resize();
|
||||||
|
private readonly mouseMoveHandler = (event: MouseEvent) => {
|
||||||
|
this.mousePosition.pointerX = event.clientX;
|
||||||
|
this.mousePosition.pointerY = event.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
private resize(): void {
|
private resize(): void {
|
||||||
const canvas = this.canvasRef.nativeElement;
|
const canvas = this.canvasRef?.nativeElement;
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initParticles(): void {
|
private initParticles(): void {
|
||||||
const count = Math.min(80, Math.floor((window.innerWidth * window.innerHeight) / 15000));
|
const particleCount = Math.min(
|
||||||
|
80,
|
||||||
|
Math.floor((window.innerWidth * window.innerHeight) / 15000)
|
||||||
|
);
|
||||||
|
|
||||||
this.particles = [];
|
this.particles = [];
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let particleIndex = 0; particleIndex < particleCount; particleIndex++) {
|
||||||
this.particles.push({
|
this.particles.push({
|
||||||
x: Math.random() * window.innerWidth,
|
positionX: Math.random() * window.innerWidth,
|
||||||
y: Math.random() * window.innerHeight,
|
positionY: Math.random() * window.innerHeight,
|
||||||
vx: (Math.random() - 0.5) * 0.4,
|
velocityX: (Math.random() - 0.5) * 0.4,
|
||||||
vy: (Math.random() - 0.5) * 0.4,
|
velocityY: (Math.random() - 0.5) * 0.4,
|
||||||
radius: Math.random() * 2 + 0.5,
|
radius: Math.random() * 2 + 0.5,
|
||||||
opacity: Math.random() * 0.5 + 0.1
|
opacity: Math.random() * 0.5 + 0.1
|
||||||
});
|
});
|
||||||
@@ -79,99 +105,102 @@ export class ParticleBgComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private animate(): void {
|
private animate(): void {
|
||||||
const canvas = this.canvasRef.nativeElement;
|
const canvas = this.canvasRef?.nativeElement;
|
||||||
|
const context = this.context;
|
||||||
|
|
||||||
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
if (!canvas || !context) {
|
||||||
|
return;
|
||||||
for (const p of this.particles) {
|
|
||||||
// Mouse interaction - repel
|
|
||||||
const dx = p.x - this.mouse.x;
|
|
||||||
const dy = p.y - this.mouse.y;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (dist < 150) {
|
|
||||||
const force = (150 - dist) / 150;
|
|
||||||
|
|
||||||
p.vx += (dx / dist) * force * 0.3;
|
|
||||||
p.vy += (dy / dist) * force * 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dampen velocity
|
|
||||||
p.vx *= 0.98;
|
|
||||||
p.vy *= 0.98;
|
|
||||||
|
|
||||||
p.x += p.vx;
|
|
||||||
p.y += p.vy;
|
|
||||||
|
|
||||||
// Wrap
|
|
||||||
if (p.x < 0)
|
|
||||||
p.x = canvas.width;
|
|
||||||
|
|
||||||
if (p.x > canvas.width)
|
|
||||||
p.x = 0;
|
|
||||||
|
|
||||||
if (p.y < 0)
|
|
||||||
p.y = canvas.height;
|
|
||||||
|
|
||||||
if (p.y > canvas.height)
|
|
||||||
p.y = 0;
|
|
||||||
|
|
||||||
// Draw particle
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
|
||||||
this.ctx.fillStyle = `rgba(139, 92, 246, ${p.opacity})`;
|
|
||||||
this.ctx.fill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw connections
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
for (let i = 0; i < this.particles.length; i++) {
|
|
||||||
for (let j = i + 1; j < this.particles.length; j++) {
|
|
||||||
const a = this.particles[i];
|
|
||||||
const b = this.particles[j];
|
|
||||||
const dx = a.x - b.x;
|
|
||||||
const dy = a.y - b.y;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (dist < 120) {
|
for (const particle of this.particles) {
|
||||||
const opacity = (1 - dist / 120) * 0.15;
|
const deltaX = particle.positionX - this.mousePosition.pointerX;
|
||||||
|
const deltaY = particle.positionY - this.mousePosition.pointerY;
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
|
||||||
this.ctx.beginPath();
|
if (distance < 150 && distance > 0) {
|
||||||
this.ctx.moveTo(a.x, a.y);
|
const force = (150 - distance) / 150;
|
||||||
this.ctx.lineTo(b.x, b.y);
|
|
||||||
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
particle.velocityX += (deltaX / distance) * force * 0.3;
|
||||||
this.ctx.lineWidth = 0.5;
|
particle.velocityY += (deltaY / distance) * force * 0.3;
|
||||||
this.ctx.stroke();
|
}
|
||||||
|
|
||||||
|
particle.velocityX *= 0.98;
|
||||||
|
particle.velocityY *= 0.98;
|
||||||
|
|
||||||
|
particle.positionX += particle.velocityX;
|
||||||
|
particle.positionY += particle.velocityY;
|
||||||
|
|
||||||
|
if (particle.positionX < 0) {
|
||||||
|
particle.positionX = canvas.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particle.positionX > canvas.width) {
|
||||||
|
particle.positionX = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particle.positionY < 0) {
|
||||||
|
particle.positionY = canvas.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particle.positionY > canvas.height) {
|
||||||
|
particle.positionY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(particle.positionX, particle.positionY, particle.radius, 0, Math.PI * 2);
|
||||||
|
context.fillStyle = `rgba(139, 92, 246, ${particle.opacity})`;
|
||||||
|
context.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let particleIndex = 0; particleIndex < this.particles.length; particleIndex++) {
|
||||||
|
for (let connectionIndex = particleIndex + 1; connectionIndex < this.particles.length; connectionIndex++) {
|
||||||
|
const sourceParticle = this.particles[particleIndex];
|
||||||
|
const targetParticle = this.particles[connectionIndex];
|
||||||
|
const deltaX = sourceParticle.positionX - targetParticle.positionX;
|
||||||
|
const deltaY = sourceParticle.positionY - targetParticle.positionY;
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
|
||||||
|
if (distance < 120) {
|
||||||
|
const opacity = (1 - distance / 120) * 0.15;
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(sourceParticle.positionX, sourceParticle.positionY);
|
||||||
|
context.lineTo(targetParticle.positionX, targetParticle.positionY);
|
||||||
|
context.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
||||||
|
context.lineWidth = 0.5;
|
||||||
|
context.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw connections to mouse
|
for (const particle of this.particles) {
|
||||||
for (const p of this.particles) {
|
const deltaX = particle.positionX - this.mousePosition.pointerX;
|
||||||
const dx = p.x - this.mouse.x;
|
const deltaY = particle.positionY - this.mousePosition.pointerY;
|
||||||
const dy = p.y - this.mouse.y;
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (dist < 200) {
|
if (distance < 200) {
|
||||||
const opacity = (1 - dist / 200) * 0.25;
|
const opacity = (1 - distance / 200) * 0.25;
|
||||||
|
|
||||||
this.ctx.beginPath();
|
context.beginPath();
|
||||||
this.ctx.moveTo(p.x, p.y);
|
context.moveTo(particle.positionX, particle.positionY);
|
||||||
this.ctx.lineTo(this.mouse.x, this.mouse.y);
|
context.lineTo(this.mousePosition.pointerX, this.mousePosition.pointerY);
|
||||||
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
context.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
||||||
this.ctx.lineWidth = 0.7;
|
context.lineWidth = 0.7;
|
||||||
this.ctx.stroke();
|
context.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.animId = requestAnimationFrame(() => this.animate());
|
this.animationId = requestAnimationFrame(() => this.animate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Particle {
|
interface Particle {
|
||||||
x: number;
|
positionX: number;
|
||||||
y: number;
|
positionY: number;
|
||||||
vx: number;
|
velocityX: number;
|
||||||
vy: number;
|
velocityY: number;
|
||||||
radius: number;
|
radius: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import { Directive, ElementRef, inject, OnInit, OnDestroy, PLATFORM_ID, Input } from '@angular/core';
|
import {
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID
|
||||||
|
} from '@angular/core';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appParallax]',
|
selector: '[appParallax]',
|
||||||
standalone: true,
|
standalone: true
|
||||||
})
|
})
|
||||||
export class ParallaxDirective implements OnInit, OnDestroy {
|
export class ParallaxDirective implements OnInit, OnDestroy {
|
||||||
private readonly el = inject(ElementRef);
|
@Input() appParallax = 0.3;
|
||||||
|
|
||||||
|
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
@Input() appParallax = 0.3; // speed factor
|
|
||||||
|
|
||||||
private scrollHandler = () => this.onScroll();
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', this.scrollHandler, { passive: true });
|
window.addEventListener('scroll', this.scrollHandler, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener('scroll', this.scrollHandler);
|
window.removeEventListener('scroll', this.scrollHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly scrollHandler = () => this.onScroll();
|
||||||
|
|
||||||
private onScroll(): void {
|
private onScroll(): void {
|
||||||
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
||||||
const scrolled = window.scrollY;
|
const scrolled = window.scrollY;
|
||||||
const rate = scrolled * this.appParallax;
|
const rate = scrolled * this.appParallax;
|
||||||
this.el.nativeElement.style.transform = `translateY(${rate}px)`;
|
|
||||||
|
this.elementRef.nativeElement.style.transform = `translateY(${rate}px)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
269
website/src/app/pages/downloads/downloads.component.html
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<div class="min-h-screen pt-32 pb-20">
|
||||||
|
<section class="container mx-auto px-6 mb-16">
|
||||||
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">Download <span class="gradient-text">Toju</span></h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed">
|
||||||
|
Available for Windows, Linux, and in your browser. Always free, always the full experience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Recommended Download -->
|
||||||
|
@if (latestRelease()) {
|
||||||
|
<section class="container mx-auto px-6 mb-16">
|
||||||
|
<div class="max-w-2xl mx-auto section-fade">
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 backdrop-blur-sm p-8 md:p-10 text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-4"
|
||||||
|
>
|
||||||
|
Recommended for you
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-foreground mb-2">Toju for {{ detectedOS().name }}</h2>
|
||||||
|
<p class="text-muted-foreground mb-6">Version {{ latestRelease()!.tag_name }}</p>
|
||||||
|
|
||||||
|
@if (recommendedUrl()) {
|
||||||
|
<a
|
||||||
|
[href]="recommendedUrl()"
|
||||||
|
class="inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Download for {{ detectedOS().name }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<p class="text-xs text-muted-foreground/60 mt-4">
|
||||||
|
Or
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="underline hover:text-muted-foreground transition-colors"
|
||||||
|
>
|
||||||
|
use the web version
|
||||||
|
</a>
|
||||||
|
- no download required.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- All Downloads for Latest Release -->
|
||||||
|
@if (latestRelease(); as release) {
|
||||||
|
<section class="container mx-auto px-6 mb-20">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h2 class="text-2xl font-bold text-foreground mb-8 section-fade">
|
||||||
|
All platforms <span class="text-muted-foreground font-normal text-lg">- {{ release.tag_name }}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid gap-3 section-fade">
|
||||||
|
@for (asset of release.assets; track asset.name) {
|
||||||
|
@if (!isMetaFile(asset.name)) {
|
||||||
|
<a
|
||||||
|
[href]="asset.browser_download_url"
|
||||||
|
class="group flex items-center justify-between gap-4 rounded-xl border border-border/30 bg-card/30 backdrop-blur-sm p-5 hover:border-purple-500/30 hover:bg-card/50 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-secondary flex items-center justify-center text-lg">
|
||||||
|
@if (getOsIcon(asset.name)) {
|
||||||
|
<img
|
||||||
|
[src]="getOsIcon(asset.name)"
|
||||||
|
[alt]="releaseService.getAssetOS(asset.name) + ' icon'"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
class="w-8 h-8 object-contain invert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground group-hover:text-purple-400 transition-colors">{{ asset.name }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ releaseService.getAssetOS(asset.name) }} · {{ releaseService.formatBytes(asset.size) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-muted-foreground group-hover:text-purple-400 transition-colors"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Historical Releases -->
|
||||||
|
@if (releases().length > 1) {
|
||||||
|
<section class="container mx-auto px-6 mb-20">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h2 class="text-2xl font-bold text-foreground mb-8 section-fade">Previous Releases</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4 section-fade">
|
||||||
|
@for (release of releases().slice(1); track release.tag_name) {
|
||||||
|
<details class="group rounded-xl border border-border/30 bg-card/30 backdrop-blur-sm overflow-hidden">
|
||||||
|
<summary class="flex items-center justify-between gap-4 p-5 cursor-pointer hover:bg-card/50 transition-colors list-none">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-secondary flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">{{ release.name || release.tag_name }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ formatDate(release.published_at) }} · {{ release.assets.length }} files</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-muted-foreground group-open:rotate-180 transition-transform"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="px-5 pb-5 space-y-2">
|
||||||
|
@if (release.body) {
|
||||||
|
<div class="text-sm text-muted-foreground mb-4 whitespace-pre-line border-b border-border/20 pb-4">{{ release.body }}</div>
|
||||||
|
}
|
||||||
|
@for (asset of release.assets; track asset.name) {
|
||||||
|
@if (!isMetaFile(asset.name) && !asset.name.toLowerCase().includes('server')) {
|
||||||
|
<a
|
||||||
|
[href]="asset.browser_download_url"
|
||||||
|
class="group/item flex items-center justify-between gap-4 rounded-lg border border-border/20 bg-background/50 p-3 hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
@if (getOsIcon(asset.name)) {
|
||||||
|
<img
|
||||||
|
[src]="getOsIcon(asset.name)"
|
||||||
|
[alt]="releaseService.getAssetOS(asset.name) + ' icon'"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain mr-1 invert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-foreground group-hover/item:text-purple-400 transition-colors">{{ asset.name }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ releaseService.formatBytes(asset.size) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
|
@if (loading()) {
|
||||||
|
<div class="container mx-auto px-6 text-center py-20">
|
||||||
|
<div class="inline-flex items-center gap-3 text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Fetching releases...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- RSS Feed link -->
|
||||||
|
<section class="container mx-auto px-6">
|
||||||
|
<div class="max-w-4xl mx-auto text-center section-fade">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 inline-block mr-1 -mt-0.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19 7.38 20 6.18 20 5 20 4 19 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Stay updated with our
|
||||||
|
<a
|
||||||
|
href="https://git.azaaxin.com/myxelium/Toju/releases.rss"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="underline hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
RSS feed
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -22,185 +22,25 @@ import { getOsIconPath } from './os-icon.util';
|
|||||||
selector: 'app-downloads',
|
selector: 'app-downloads',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [AdSlotComponent],
|
imports: [AdSlotComponent],
|
||||||
template: `
|
templateUrl: './downloads.component.html'
|
||||||
<div class="min-h-screen pt-32 pb-20">
|
|
||||||
<section class="container mx-auto px-6 mb-16">
|
|
||||||
<div class="max-w-3xl mx-auto text-center">
|
|
||||||
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">
|
|
||||||
Download <span class="gradient-text">Toju</span>
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-muted-foreground leading-relaxed">
|
|
||||||
Available for Windows, Linux, and in your browser. Always free, always the full experience.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Recommended Download -->
|
|
||||||
@if (latestRelease()) {
|
|
||||||
<section class="container mx-auto px-6 mb-16">
|
|
||||||
<div class="max-w-2xl mx-auto section-fade">
|
|
||||||
<div class="rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 backdrop-blur-sm p-8 md:p-10 text-center">
|
|
||||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-4">
|
|
||||||
Recommended for you
|
|
||||||
</div>
|
|
||||||
<h2 class="text-2xl font-bold text-foreground mb-2">
|
|
||||||
Toju for {{ detectedOS().name }}
|
|
||||||
</h2>
|
|
||||||
<p class="text-muted-foreground mb-6">Version {{ latestRelease()!.tag_name }}</p>
|
|
||||||
|
|
||||||
@if (recommendedUrl()) {
|
|
||||||
<a
|
|
||||||
[href]="recommendedUrl()"
|
|
||||||
class="inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40"
|
|
||||||
>
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
||||||
Download for {{ detectedOS().name }}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
<p class="text-xs text-muted-foreground/60 mt-4">
|
|
||||||
Or <a href="https://web.toju.app/" target="_blank" rel="noopener" class="underline hover:text-muted-foreground transition-colors">use the web version</a> - no download required.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- All Downloads for Latest Release -->
|
|
||||||
@if (latestRelease(); as release) {
|
|
||||||
<section class="container mx-auto px-6 mb-20">
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<h2 class="text-2xl font-bold text-foreground mb-8 section-fade">
|
|
||||||
All platforms <span class="text-muted-foreground font-normal text-lg">- {{ release.tag_name }}</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="grid gap-3 section-fade">
|
|
||||||
@for (asset of release.assets; track asset.name) {
|
|
||||||
@if (!isMetaFile(asset.name)) {
|
|
||||||
<a
|
|
||||||
[href]="asset.browser_download_url"
|
|
||||||
class="group flex items-center justify-between gap-4 rounded-xl border border-border/30 bg-card/30 backdrop-blur-sm p-5 hover:border-purple-500/30 hover:bg-card/50 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-secondary flex items-center justify-center text-lg">
|
|
||||||
@if(getOsIcon(asset.name)){
|
|
||||||
<img
|
|
||||||
[src]="getOsIcon(asset.name)"
|
|
||||||
[alt]="releaseService.getAssetOS(asset.name) + ' icon'"
|
|
||||||
width="32" height="32"
|
|
||||||
class="w-8 h-8 object-contain invert"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground group-hover:text-purple-400 transition-colors">{{ asset.name }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ releaseService.getAssetOS(asset.name) }} · {{ releaseService.formatBytes(asset.size) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-muted-foreground group-hover:text-purple-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Historical Releases -->
|
|
||||||
@if (releases().length > 1) {
|
|
||||||
<section class="container mx-auto px-6 mb-20">
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<h2 class="text-2xl font-bold text-foreground mb-8 section-fade">Previous Releases</h2>
|
|
||||||
|
|
||||||
<div class="space-y-4 section-fade">
|
|
||||||
@for (release of releases().slice(1); track release.tag_name) {
|
|
||||||
<details class="group rounded-xl border border-border/30 bg-card/30 backdrop-blur-sm overflow-hidden">
|
|
||||||
<summary class="flex items-center justify-between gap-4 p-5 cursor-pointer hover:bg-card/50 transition-colors list-none">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-8 h-8 rounded-lg bg-secondary flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">{{ release.name || release.tag_name }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ formatDate(release.published_at) }} · {{ release.assets.length }} files</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg class="w-4 h-4 text-muted-foreground group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
||||||
</summary>
|
|
||||||
<div class="px-5 pb-5 space-y-2">
|
|
||||||
@if (release.body) {
|
|
||||||
<div class="text-sm text-muted-foreground mb-4 whitespace-pre-line border-b border-border/20 pb-4">{{ release.body }}</div>
|
|
||||||
}
|
|
||||||
@for (asset of release.assets; track asset.name) {
|
|
||||||
@if (!isMetaFile(asset.name) && !asset.name.toLowerCase().includes('server')) {
|
|
||||||
<a
|
|
||||||
[href]="asset.browser_download_url"
|
|
||||||
class="group/item flex items-center justify-between gap-4 rounded-lg border border-border/20 bg-background/50 p-3 hover:border-purple-500/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-sm">{{ getOsIcon(asset.name) }}</span>
|
|
||||||
@if(getOsIcon(asset.name)){
|
|
||||||
<span class="text-sm"></span>
|
|
||||||
<img
|
|
||||||
[src]="getOsIcon(asset.name)"
|
|
||||||
[alt]="releaseService.getAssetOS(asset.name) + ' icon'"
|
|
||||||
width="16" height="16"
|
|
||||||
class="w-4 h-4 object-contain mr-1 invert"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium text-foreground group-hover/item:text-purple-400 transition-colors">{{ asset.name }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ releaseService.formatBytes(asset.size) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg class="w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Loading state -->
|
|
||||||
@if (loading()) {
|
|
||||||
<div class="container mx-auto px-6 text-center py-20">
|
|
||||||
<div class="inline-flex items-center gap-3 text-muted-foreground">
|
|
||||||
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
|
||||||
Fetching releases...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- RSS Feed link -->
|
|
||||||
<section class="container mx-auto px-6">
|
|
||||||
<div class="max-w-4xl mx-auto text-center section-fade">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
<svg class="w-4 h-4 inline-block mr-1 -mt-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19 7.38 20 6.18 20 5 20 4 19 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/></svg>
|
|
||||||
Stay updated with our <a href="https://git.azaaxin.com/myxelium/Toju/releases.rss" target="_blank" rel="noopener" class="underline hover:text-foreground transition-colors">RSS feed</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class DownloadsComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class DownloadsComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
readonly releaseService = inject(ReleaseService);
|
readonly releaseService = inject(ReleaseService);
|
||||||
|
readonly releases = signal<Release[]>([]);
|
||||||
|
readonly latestRelease = signal<Release | null>(null);
|
||||||
|
readonly detectedOS = signal<DetectedOS>({
|
||||||
|
name: 'Linux',
|
||||||
|
icon: '🐧',
|
||||||
|
filePattern: /\.AppImage$/i,
|
||||||
|
ymlFile: 'latest-linux.yml'
|
||||||
|
});
|
||||||
|
readonly recommendedUrl = signal<string | null>(null);
|
||||||
|
readonly loading = signal(true);
|
||||||
|
|
||||||
private readonly seoService = inject(SeoService);
|
private readonly seoService = inject(SeoService);
|
||||||
private readonly scrollAnimation = inject(ScrollAnimationService);
|
private readonly scrollAnimation = inject(ScrollAnimationService);
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
readonly releases = signal<Release[]>([]);
|
|
||||||
readonly latestRelease = signal<Release | null>(null);
|
|
||||||
readonly detectedOS = signal<DetectedOS>({ name: 'Linux', icon: '🐧', filePattern: /\.AppImage$/i, ymlFile: 'latest-linux.yml' });
|
|
||||||
readonly recommendedUrl = signal<string | null>(null);
|
|
||||||
readonly loading = signal(true);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.update({
|
this.seoService.update({
|
||||||
title: 'Download Toju',
|
title: 'Download Toju',
|
||||||
@@ -216,10 +56,13 @@ export class DownloadsComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.releases.set(releases);
|
this.releases.set(releases);
|
||||||
|
|
||||||
if (releases.length > 0) {
|
if (releases.length > 0) {
|
||||||
this.latestRelease.set(releases[0]);
|
const latestRelease = releases[0];
|
||||||
const asset = releases[0].assets.find(a => os.filePattern.test(a.name) && !a.name.toLowerCase().includes('server'));
|
const recommendedAsset = latestRelease.assets.find(
|
||||||
|
(releaseAsset) => os.filePattern.test(releaseAsset.name) && !releaseAsset.name.toLowerCase().includes('server')
|
||||||
|
);
|
||||||
|
|
||||||
this.recommendedUrl.set(asset?.browser_download_url ?? null);
|
this.latestRelease.set(latestRelease);
|
||||||
|
this.recommendedUrl.set(recommendedAsset?.browser_download_url ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
@@ -243,12 +86,16 @@ export class DownloadsComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOsIcon(name: string, size = 64): string {
|
getOsIcon(name: string, size = 64): string {
|
||||||
return getOsIconPath(this.releaseService.getAssetOS(name), size);
|
return getOsIconPath(name, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate(dateStr: string): string {
|
formatDate(dateStr: string): string {
|
||||||
try {
|
try {
|
||||||
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return dateStr;
|
return dateStr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,68 @@
|
|||||||
export function getOsIconPath(os: string, size = 64): string {
|
const WINDOWS_SUFFIXES = ['.exe', '.msi'];
|
||||||
switch (os) {
|
const WINDOWS_HINTS = ['win', 'windows'];
|
||||||
case 'Windows':
|
const MAC_SUFFIXES = ['.dmg', '.pkg'];
|
||||||
return `/images/windows/${size}x${size}.png`;
|
const MAC_HINTS = [
|
||||||
case 'macOS':
|
'mac',
|
||||||
return `/images/macos/${size}x${size}.png`;
|
'macos',
|
||||||
case 'Linux':
|
'osx',
|
||||||
case 'Linux (deb)':
|
'darwin'
|
||||||
return `/images/linux/${size}x${size}.png`;
|
];
|
||||||
default:
|
const LINUX_SUFFIXES = [
|
||||||
return '';
|
'.appimage',
|
||||||
}
|
'.deb',
|
||||||
|
'.rpm'
|
||||||
|
];
|
||||||
|
const LINUX_HINTS = [
|
||||||
|
'linux',
|
||||||
|
'appimage',
|
||||||
|
'.deb',
|
||||||
|
'.rpm'
|
||||||
|
];
|
||||||
|
const ARCHIVE_SUFFIXES = [
|
||||||
|
'.zip',
|
||||||
|
'.tar',
|
||||||
|
'.tar.gz',
|
||||||
|
'.tgz',
|
||||||
|
'.tar.xz',
|
||||||
|
'.7z',
|
||||||
|
'.rar'
|
||||||
|
];
|
||||||
|
const ARCHIVE_HINTS = [
|
||||||
|
'archive',
|
||||||
|
'.zip',
|
||||||
|
'.tar',
|
||||||
|
'.7z',
|
||||||
|
'.rar'
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSizedIconPath(folder: string, size: number): string {
|
||||||
|
return `/images/${folder}/${size}x${size}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesAny(value: string, hints: string[]): boolean {
|
||||||
|
const tokens = value.split(/[^a-z0-9]+/).filter(Boolean);
|
||||||
|
|
||||||
|
return hints.some((hint) => tokens.includes(hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesIconPattern(value: string, suffixes: string[], hints: string[] = []): boolean {
|
||||||
|
return suffixes.some((suffix) => value.endsWith(suffix)) || includesAny(value, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOsIconPath(nameOrOs: string, size = 64): string {
|
||||||
|
const normalized = nameOrOs.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (matchesIconPattern(normalized, WINDOWS_SUFFIXES, WINDOWS_HINTS))
|
||||||
|
return getSizedIconPath('windows', size);
|
||||||
|
|
||||||
|
if (matchesIconPattern(normalized, MAC_SUFFIXES, MAC_HINTS))
|
||||||
|
return getSizedIconPath('macos', size);
|
||||||
|
|
||||||
|
if (matchesIconPattern(normalized, LINUX_SUFFIXES, LINUX_HINTS))
|
||||||
|
return getSizedIconPath('linux', size);
|
||||||
|
|
||||||
|
if (matchesIconPattern(normalized, ARCHIVE_SUFFIXES, ARCHIVE_HINTS))
|
||||||
|
return '/images/misc/zip.png';
|
||||||
|
|
||||||
|
return '/images/misc/file.png';
|
||||||
}
|
}
|
||||||
|
|||||||
95
website/src/app/pages/gallery/gallery.component.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<div class="min-h-screen pt-32 pb-20">
|
||||||
|
<section class="container mx-auto px-6 mb-16">
|
||||||
|
<div class="max-w-3xl mx-auto text-center section-fade">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6"
|
||||||
|
>
|
||||||
|
Image Gallery
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">A closer look at <span class="gradient-text">Toju</span></h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed">
|
||||||
|
Explore screenshots of the app experience, from voice chat and media sharing to servers, rooms, and full-screen collaboration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container mx-auto px-6 mb-20">
|
||||||
|
<div class="max-w-6xl mx-auto section-fade">
|
||||||
|
<div class="relative overflow-hidden rounded-3xl border border-border/30 bg-card/30 backdrop-blur-sm">
|
||||||
|
<div class="relative aspect-[16/9]">
|
||||||
|
<img
|
||||||
|
ngSrc="/images/screenshots/screenshot_main.png"
|
||||||
|
alt="Toju main application screenshot"
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
sizes="(min-width: 1536px) 75vw, (min-width: 1280px) 90vw, 100vw"
|
||||||
|
class="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-background via-background/80 to-transparent p-6 md:p-8">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-purple-400 mb-2">Featured</p>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-2">The full Toju workspace</h2>
|
||||||
|
<p class="max-w-2xl text-sm md:text-base text-muted-foreground">
|
||||||
|
See the main interface where rooms, messages, presence, and media all come together in one focused layout.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<section class="container mx-auto px-6 mb-20">
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
@for (item of galleryItems; track item.src) {
|
||||||
|
<a
|
||||||
|
[href]="item.src"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="section-fade group overflow-hidden rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm hover:border-purple-500/30 hover:bg-card/50 transition-all"
|
||||||
|
>
|
||||||
|
<div class="relative aspect-video overflow-hidden">
|
||||||
|
<img
|
||||||
|
[ngSrc]="item.src"
|
||||||
|
[alt]="item.title"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 33vw"
|
||||||
|
class="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-5">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-2">{{ item.title }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground leading-relaxed">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container mx-auto px-6">
|
||||||
|
<div
|
||||||
|
class="max-w-4xl mx-auto section-fade rounded-3xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 p-8 md:p-10 text-center"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-4">Want to see it in action?</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed mb-6">Download Toju or jump into the browser experience and explore the interface yourself.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25"
|
||||||
|
>
|
||||||
|
Go to downloads
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Open web version
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
91
website/src/app/pages/gallery/gallery.component.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import { isPlatformBrowser, NgOptimizedImage } from '@angular/common';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { AdSlotComponent } from '../../components/ad-slot/ad-slot.component';
|
||||||
|
import { ScrollAnimationService } from '../../services/scroll-animation.service';
|
||||||
|
import { SeoService } from '../../services/seo.service';
|
||||||
|
|
||||||
|
interface GalleryItem {
|
||||||
|
src: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-gallery',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgOptimizedImage,
|
||||||
|
RouterLink,
|
||||||
|
AdSlotComponent
|
||||||
|
],
|
||||||
|
templateUrl: './gallery.component.html'
|
||||||
|
})
|
||||||
|
export class GalleryComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
readonly galleryItems: GalleryItem[] = [
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/screenshot_main.png',
|
||||||
|
title: 'Main chat view',
|
||||||
|
description: 'The core Toju experience with channels, messages, and direct communication tools.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/screenshare_gaming.png',
|
||||||
|
title: 'Gaming screen share',
|
||||||
|
description: 'Share gameplay, guides, and live moments with smooth full-resolution screen sharing.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/serverViewScreen.png',
|
||||||
|
title: 'Server overview',
|
||||||
|
description: 'Navigate servers and rooms with a layout designed for clarity and speed.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/music.png',
|
||||||
|
title: 'Music and voice',
|
||||||
|
description: 'Stay in sync with voice and media features in a focused, low-friction interface.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/videos.png',
|
||||||
|
title: 'Video sharing',
|
||||||
|
description: 'Preview and share visual content directly with your friends and communities.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/filedownload.png',
|
||||||
|
title: 'File transfers',
|
||||||
|
description: 'Move files quickly without artificial size limits or unnecessary hoops.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/images/screenshots/gif.png',
|
||||||
|
title: 'Rich media chat',
|
||||||
|
description: 'Conversations stay lively with visual media support built right in.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly seoService = inject(SeoService);
|
||||||
|
private readonly scrollAnimation = inject(ScrollAnimationService);
|
||||||
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.seoService.update({
|
||||||
|
title: 'Toju Image Gallery',
|
||||||
|
description: 'Browse screenshots of Toju and explore the interface for chat, file sharing, voice, and screen sharing.',
|
||||||
|
url: 'https://toju.app/gallery'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
setTimeout(() => this.scrollAnimation.init(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.scrollAnimation.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
542
website/src/app/pages/home/home.component.html
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
<!-- Hero -->
|
||||||
|
<section class="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||||
|
<!-- Gradient orbs -->
|
||||||
|
<div
|
||||||
|
[appParallax]="0.15"
|
||||||
|
class="absolute top-1/4 -left-32 w-96 h-96 bg-purple-600/20 rounded-full blur-[128px] animate-float"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
[appParallax]="0.25"
|
||||||
|
class="absolute bottom-1/4 -right-32 w-80 h-80 bg-violet-500/15 rounded-full blur-[100px] animate-float"
|
||||||
|
style="animation-delay: -3s"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 container mx-auto px-6 text-center pt-32 pb-20">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 text-purple-400 text-sm font-medium mb-8 animate-fade-in"
|
||||||
|
>
|
||||||
|
<span class="w-2 h-2 rounded-full bg-purple-400 animate-pulse"></span>
|
||||||
|
Currently in Beta - Free & Open Source
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-5xl md:text-7xl lg:text-8xl font-extrabold tracking-tight mb-6 animate-fade-in-up">
|
||||||
|
<span class="text-foreground">Talk freely.</span><br />
|
||||||
|
<span class="gradient-text">Own your voice.</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 leading-relaxed animate-fade-in-up"
|
||||||
|
style="animation-delay: 0.2s"
|
||||||
|
>
|
||||||
|
Crystal-clear voice calls, unlimited screen sharing, and file transfers with no size limits. Peer-to-peer. Private. Completely free.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- CTA Buttons -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6 animate-fade-in-up"
|
||||||
|
style="animation-delay: 0.4s"
|
||||||
|
>
|
||||||
|
@if (downloadUrl()) {
|
||||||
|
<a
|
||||||
|
[href]="downloadUrl()"
|
||||||
|
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40 animate-glow-pulse"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Download for {{ detectedOS().name }}
|
||||||
|
<span class="text-sm opacity-75">{{ detectedOS().icon }}</span>
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Download Toju
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium text-lg hover:bg-card hover:border-purple-500/30 transition-all backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
Open in Browser
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 opacity-60"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (latestVersion()) {
|
||||||
|
<p
|
||||||
|
class="text-xs text-muted-foreground/60 animate-fade-in"
|
||||||
|
style="animation-delay: 0.6s"
|
||||||
|
>
|
||||||
|
Version {{ latestVersion() }} ·
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="underline hover:text-muted-foreground transition-colors"
|
||||||
|
>All platforms</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Scroll indicator -->
|
||||||
|
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-muted-foreground/40"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Features Grid -->
|
||||||
|
<section class="relative py-32">
|
||||||
|
<div class="container mx-auto px-6">
|
||||||
|
<div class="text-center mb-20 section-fade">
|
||||||
|
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-4">
|
||||||
|
Everything you need,<br />
|
||||||
|
<span class="gradient-text">nothing you don't.</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted-foreground text-lg max-w-xl mx-auto">No bloat. No paywalls. Just the tools to connect with the people who matter.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Voice Calls -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-6 group-hover:bg-purple-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-purple-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">HD Voice Calls</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Crystal-clear audio with built-in noise reduction. Hear every word, not the background. No quality compromises, ever.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Screen Sharing -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
style="transition-delay: 0.1s"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-violet-500/10 flex items-center justify-center mb-6 group-hover:bg-violet-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-violet-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">Screen Sharing</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Share your screen at full resolution. No time limits, no quality downgrades. Perfect for pair programming, presentations, or showing your
|
||||||
|
epic gameplay.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Sharing -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
style="transition-delay: 0.2s"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-6 group-hover:bg-pink-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-pink-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">Unlimited File Sharing</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Send files of any size directly to your friends. No upload limits, no compression. Your files go straight from you to them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center mb-6 group-hover:bg-emerald-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-emerald-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">True Privacy</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Peer-to-peer means your data goes directly between you and your friends. No servers storing your conversations. Your business is your
|
||||||
|
business.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Open Source -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
style="transition-delay: 0.1s"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center mb-6 group-hover:bg-blue-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-blue-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">Open Source</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Every line of code is public. Audit it, modify it, host your own signal server. Full transparency - nothing is hidden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Free -->
|
||||||
|
<div
|
||||||
|
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||||
|
style="transition-delay: 0.2s"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-6 group-hover:bg-amber-500/20 transition-colors">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-amber-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">Completely Free</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
No premium tiers. No paywalls. No "starter plans". Every feature is available to everyone, always. Made with love, not profit margins.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Gaming Section -->
|
||||||
|
<section class="relative py-32">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-purple-950/10 to-transparent"></div>
|
||||||
|
<div class="relative container mx-auto px-6">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||||
|
<div class="section-fade">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Built for Gamers
|
||||||
|
</div>
|
||||||
|
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
||||||
|
Your perfect<br />
|
||||||
|
<span class="gradient-text">gaming companion.</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||||
|
Ultra-low latency voice chat that doesn't eat your bandwidth. Share your screen without frame drops. Send clips and files instantly. All
|
||||||
|
while keeping your CPU free for what matters - winning.
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<li class="flex items-center gap-3 text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Low-latency peer-to-peer voice - no relay servers in the way</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>AI-powered noise suppression - keyboard clatter stays out</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Full-resolution screen sharing at high FPS</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 text-muted-foreground">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Send replays and screenshots with no file size limit</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-fade relative">
|
||||||
|
<div class="relative rounded-2xl overflow-hidden border border-border/30 bg-card/30 backdrop-blur-sm aspect-video">
|
||||||
|
<img
|
||||||
|
ngSrc="/images/screenshots/screenshare_gaming.png"
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
alt="Toju gaming screen sharing preview"
|
||||||
|
class="object-cover"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent"></div>
|
||||||
|
<div class="absolute bottom-4 left-4 text-sm text-muted-foreground/60">Game on. No limits.</div>
|
||||||
|
</div>
|
||||||
|
<!-- Glow effect -->
|
||||||
|
<div class="absolute -inset-4 bg-purple-600/5 rounded-3xl blur-2xl -z-10"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Self-hostable Section -->
|
||||||
|
<section class="relative py-32">
|
||||||
|
<div class="container mx-auto px-6">
|
||||||
|
<div class="section-fade max-w-3xl mx-auto text-center">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-medium mb-6"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Self-Hostable
|
||||||
|
</div>
|
||||||
|
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
||||||
|
Your infrastructure,<br />
|
||||||
|
<span class="gradient-text">your rules.</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||||
|
Toju uses a lightweight coordination server to help peers find each other - that's it. Your actual conversations never touch a server. Want
|
||||||
|
even more control? Run your own coordination server in minutes. Full independence, zero compromises.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
routerLink="/what-is-toju"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Learn how it works
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://git.azaaxin.com/myxelium/Toju"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 text-muted-foreground hover:text-foreground transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/gitea.png"
|
||||||
|
alt=""
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
class="w-4 h-4 object-contain"
|
||||||
|
/>
|
||||||
|
View source code
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Banner -->
|
||||||
|
<section class="relative py-24">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-r from-purple-950/20 via-violet-950/30 to-purple-950/20"></div>
|
||||||
|
<div class="relative container mx-auto px-6 text-center section-fade">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-4">Ready to take back your conversations?</h2>
|
||||||
|
<p class="text-muted-foreground text-lg mb-8 max-w-lg mx-auto">Join thousands choosing privacy, freedom, and real connection.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
@if (downloadUrl()) {
|
||||||
|
<a
|
||||||
|
[href]="downloadUrl()"
|
||||||
|
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
||||||
|
>
|
||||||
|
Download for {{ detectedOS().name }}
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
||||||
|
>
|
||||||
|
Download Toju
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Try in Browser
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
PLATFORM_ID,
|
PLATFORM_ID,
|
||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser, NgOptimizedImage } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { ReleaseService, DetectedOS } from '../../services/release.service';
|
import { ReleaseService, DetectedOS } from '../../services/release.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
@@ -19,288 +19,34 @@ import { ParallaxDirective } from '../../directives/parallax.directive';
|
|||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
|
NgOptimizedImage,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
AdSlotComponent,
|
AdSlotComponent,
|
||||||
ParallaxDirective
|
ParallaxDirective
|
||||||
],
|
],
|
||||||
template: `
|
templateUrl: './home.component.html'
|
||||||
<!-- Hero -->
|
|
||||||
<section class="relative min-h-screen flex items-center justify-center overflow-hidden">
|
|
||||||
<!-- Gradient orbs -->
|
|
||||||
<div [appParallax]="0.15" class="absolute top-1/4 -left-32 w-96 h-96 bg-purple-600/20 rounded-full blur-[128px] animate-float"></div>
|
|
||||||
<div [appParallax]="0.25" class="absolute bottom-1/4 -right-32 w-80 h-80 bg-violet-500/15 rounded-full blur-[100px] animate-float" style="animation-delay: -3s;"></div>
|
|
||||||
|
|
||||||
<div class="relative z-10 container mx-auto px-6 text-center pt-32 pb-20">
|
|
||||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 text-purple-400 text-sm font-medium mb-8 animate-fade-in">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-purple-400 animate-pulse"></span>
|
|
||||||
Currently in Beta - Free & Open Source
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="text-5xl md:text-7xl lg:text-8xl font-extrabold tracking-tight mb-6 animate-fade-in-up">
|
|
||||||
<span class="text-foreground">Talk freely.</span><br>
|
|
||||||
<span class="gradient-text">Own your voice.</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p class="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 leading-relaxed animate-fade-in-up" style="animation-delay: 0.2s;">
|
|
||||||
Crystal-clear voice calls, unlimited screen sharing, and file transfers with no size limits.
|
|
||||||
Peer-to-peer. Private. Completely free.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTA Buttons -->
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6 animate-fade-in-up" style="animation-delay: 0.4s;">
|
|
||||||
@if (downloadUrl()) {
|
|
||||||
<a
|
|
||||||
[href]="downloadUrl()"
|
|
||||||
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40 animate-glow-pulse"
|
|
||||||
>
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
||||||
Download for {{ detectedOS().name }}
|
|
||||||
<span class="text-sm opacity-75">{{ detectedOS().icon }}</span>
|
|
||||||
</a>
|
|
||||||
} @else {
|
|
||||||
<a
|
|
||||||
routerLink="/downloads"
|
|
||||||
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40"
|
|
||||||
>
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
||||||
Download Toju
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://web.toju.app/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium text-lg hover:bg-card hover:border-purple-500/30 transition-all backdrop-blur-sm"
|
|
||||||
>
|
|
||||||
Open in Browser
|
|
||||||
<svg class="w-5 h-5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (latestVersion()) {
|
|
||||||
<p class="text-xs text-muted-foreground/60 animate-fade-in" style="animation-delay: 0.6s;">
|
|
||||||
Version {{ latestVersion() }} · <a routerLink="/downloads" class="underline hover:text-muted-foreground transition-colors">All platforms</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Scroll indicator -->
|
|
||||||
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
|
||||||
<svg class="w-6 h-6 text-muted-foreground/40" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Features Grid -->
|
|
||||||
<section class="relative py-32">
|
|
||||||
<div class="container mx-auto px-6">
|
|
||||||
<div class="text-center mb-20 section-fade">
|
|
||||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-4">Everything you need,<br><span class="gradient-text">nothing you don't.</span></h2>
|
|
||||||
<p class="text-muted-foreground text-lg max-w-xl mx-auto">No bloat. No paywalls. Just the tools to connect with the people who matter.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<!-- Voice Calls -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-6 group-hover:bg-purple-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">HD Voice Calls</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">Crystal-clear audio with built-in noise reduction. Hear every word, not the background. No quality compromises, ever.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Screen Sharing -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300" style="transition-delay: 0.1s;">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-violet-500/10 flex items-center justify-center mb-6 group-hover:bg-violet-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">Screen Sharing</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">Share your screen at full resolution. No time limits, no quality downgrades. Perfect for pair programming, presentations, or showing your epic gameplay.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- File Sharing -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300" style="transition-delay: 0.2s;">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-6 group-hover:bg-pink-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-pink-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">Unlimited File Sharing</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">Send files of any size directly to your friends. No upload limits, no compression. Your files go straight from you to them.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Privacy -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center mb-6 group-hover:bg-emerald-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">True Privacy</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">Peer-to-peer means your data goes directly between you and your friends. No servers storing your conversations. Your business is your business.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Open Source -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300" style="transition-delay: 0.1s;">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center mb-6 group-hover:bg-blue-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">Open Source</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">Every line of code is public. Audit it, modify it, host your own signal server. Full transparency - nothing is hidden.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Free -->
|
|
||||||
<div class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300" style="transition-delay: 0.2s;">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-6 group-hover:bg-amber-500/20 transition-colors">
|
|
||||||
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">Completely Free</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">No premium tiers. No paywalls. No "starter plans". Every feature is available to everyone, always. Made with love, not profit margins.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Gaming Section -->
|
|
||||||
<section class="relative py-32">
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-purple-950/10 to-transparent"></div>
|
|
||||||
<div class="relative container mx-auto px-6">
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
|
||||||
<div class="section-fade">
|
|
||||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
||||||
Built for Gamers
|
|
||||||
</div>
|
|
||||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
|
||||||
Your perfect<br><span class="gradient-text">gaming companion.</span>
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
|
||||||
Ultra-low latency voice chat that doesn't eat your bandwidth. Share your screen without frame drops. Send clips and files instantly. All while keeping your CPU free for what matters - winning.
|
|
||||||
</p>
|
|
||||||
<ul class="space-y-4">
|
|
||||||
<li class="flex items-center gap-3 text-muted-foreground">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>Low-latency peer-to-peer voice - no relay servers in the way</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-muted-foreground">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>AI-powered noise suppression - keyboard clatter stays out</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-muted-foreground">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>Full-resolution screen sharing at high FPS</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-muted-foreground">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>Send replays and screenshots with no file size limit</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-fade relative">
|
|
||||||
<div class="relative rounded-2xl overflow-hidden border border-border/30 bg-card/30 backdrop-blur-sm aspect-video">
|
|
||||||
<video
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
playsinline
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
poster=""
|
|
||||||
>
|
|
||||||
<source src="https://cdn.pixabay.com/video/2024/04/08/207248_large.mp4" type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent"></div>
|
|
||||||
<div class="absolute bottom-4 left-4 text-sm text-muted-foreground/60">
|
|
||||||
Game on. No limits.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Glow effect -->
|
|
||||||
<div class="absolute -inset-4 bg-purple-600/5 rounded-3xl blur-2xl -z-10"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Self-hostable Section -->
|
|
||||||
<section class="relative py-32">
|
|
||||||
<div class="container mx-auto px-6">
|
|
||||||
<div class="section-fade max-w-3xl mx-auto text-center">
|
|
||||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-medium mb-6">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg>
|
|
||||||
Self-Hostable
|
|
||||||
</div>
|
|
||||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
|
||||||
Your infrastructure,<br><span class="gradient-text">your rules.</span>
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
|
||||||
Toju uses a lightweight coordination server to help peers find each other - that's it. Your actual conversations never touch a server.
|
|
||||||
Want even more control? Run your own coordination server in minutes. Full independence, zero compromises.
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
||||||
<a routerLink="/what-is-toju" class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all">
|
|
||||||
Learn how it works
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-6 py-3 text-muted-foreground hover:text-foreground transition-colors text-sm">
|
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
||||||
View source code
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- CTA Banner -->
|
|
||||||
<section class="relative py-24">
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-purple-950/20 via-violet-950/30 to-purple-950/20"></div>
|
|
||||||
<div class="relative container mx-auto px-6 text-center section-fade">
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-4">Ready to take back your conversations?</h2>
|
|
||||||
<p class="text-muted-foreground text-lg mb-8 max-w-lg mx-auto">Join thousands choosing privacy, freedom, and real connection.</p>
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
||||||
@if (downloadUrl()) {
|
|
||||||
<a
|
|
||||||
[href]="downloadUrl()"
|
|
||||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
|
||||||
>
|
|
||||||
Download for {{ detectedOS().name }}
|
|
||||||
</a>
|
|
||||||
} @else {
|
|
||||||
<a
|
|
||||||
routerLink="/downloads"
|
|
||||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
|
||||||
>
|
|
||||||
Download Toju
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
<a
|
|
||||||
href="https://web.toju.app/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
|
||||||
>
|
|
||||||
Try in Browser
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
readonly detectedOS = signal<DetectedOS>({
|
||||||
|
name: 'Linux',
|
||||||
|
icon: '🐧',
|
||||||
|
filePattern: /\.AppImage$/i,
|
||||||
|
ymlFile: 'latest-linux.yml'
|
||||||
|
});
|
||||||
|
readonly downloadUrl = signal<string | null>(null);
|
||||||
|
readonly latestVersion = signal<string | null>(null);
|
||||||
|
|
||||||
private readonly releaseService = inject(ReleaseService);
|
private readonly releaseService = inject(ReleaseService);
|
||||||
private readonly seoService = inject(SeoService);
|
private readonly seoService = inject(SeoService);
|
||||||
private readonly scrollAnimation = inject(ScrollAnimationService);
|
private readonly scrollAnimation = inject(ScrollAnimationService);
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
readonly detectedOS = signal<DetectedOS>({ name: 'Linux', icon: '🐧', filePattern: /\.AppImage$/i, ymlFile: 'latest-linux.yml' });
|
|
||||||
readonly downloadUrl = signal<string | null>(null);
|
|
||||||
readonly latestVersion = signal<string | null>(null);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.update({
|
this.seoService.update({
|
||||||
title: 'Free Peer-to-Peer Voice, Video & Chat',
|
title: 'Free Peer-to-Peer Voice, Video & Chat',
|
||||||
description: 'Toju is a free, open-source, peer-to-peer communication app. Crystal-clear voice calls, unlimited screen sharing, no file size limits, complete privacy.',
|
description:
|
||||||
|
'Toju is a free, open-source, peer-to-peer communication app. Crystal-clear voice calls, unlimited screen sharing, '
|
||||||
|
+ 'no file size limits, complete privacy.',
|
||||||
url: 'https://toju.app/'
|
url: 'https://toju.app/'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
214
website/src/app/pages/philosophy/philosophy.component.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<div class="min-h-screen pt-32 pb-20">
|
||||||
|
<!-- Hero -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6"
|
||||||
|
>
|
||||||
|
Our Manifesto
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">Why we <span class="gradient-text">build</span> Toju</h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed">A letter from the people behind the project.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<article class="max-w-3xl mx-auto prose prose-invert prose-lg">
|
||||||
|
<!-- Ownership -->
|
||||||
|
<div class="section-fade mb-16">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6 !mt-0">We Lost Something Important</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Over the past two decades, something fundamental shifted. Our conversations, our memories, our connections - they stopped belonging to us.
|
||||||
|
They live on servers we don't control, inside apps that treat our personal lives as data to be harvested, analyzed, and sold to the highest
|
||||||
|
bidder.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
We gave up ownership of our digital lives so gradually that most of us didn't even notice. A "free" app here, a convenient service there -
|
||||||
|
each one taking a little more of our privacy in exchange for convenience. Toju exists because we believe it's time to take it back.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No predatory pricing -->
|
||||||
|
<div class="section-fade mb-16">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">No Paywalls. No Premium Tiers. Ever.</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
You know the playbook: launch a free product, build a user base, then start locking features behind subscription tiers. Can't share your
|
||||||
|
screen at more than 720p unless you upgrade. File size limited to 8 MB. Want noise suppression? That's a premium feature now.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
We refuse to play that game. <strong class="text-foreground">Every feature in Toju is available to every user, always.</strong>
|
||||||
|
There is no "Toju Nitro," no "Pro plan," no artificial limitations designed to push you toward your wallet. Communication is a human need,
|
||||||
|
not a luxury - and the tools for it should reflect that.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Privacy as a right -->
|
||||||
|
<div class="section-fade mb-16">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Privacy Is a Right, Not a Feature</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Most communication platforms collect everything: who you talk to, when, for how long, what you share. They build profiles of your social
|
||||||
|
graph, your habits, your interests. Even services that claim to care about privacy still store metadata on their servers.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Toju is architecturally different. Your data goes directly from your device to your friend's device. We don't have your messages. We don't
|
||||||
|
have your files. We don't have your call history. Not because we promised not to look - but because the data never touches our
|
||||||
|
infrastructure. We built the technology so that
|
||||||
|
<strong class="text-foreground">privacy isn't something we offer; it's something we literally cannot violate.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Better world -->
|
||||||
|
<div class="section-fade mb-16">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Built from the Heart</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Toju wasn't born in a boardroom with revenue projections. It was born from frustration - frustration with being the product, with watching
|
||||||
|
friends get locked out of features they used to have for free, with the growing feeling that the tools we depend on daily don't actually
|
||||||
|
serve our interests.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
We build Toju because we genuinely want to make the world a little better. The internet was supposed to connect people freely, and somewhere
|
||||||
|
along the way, that mission got hijacked by business models that exploit the very connections they facilitate.
|
||||||
|
<strong class="text-foreground">Toju is our small act of reclaiming that original promise.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Open source -->
|
||||||
|
<div class="section-fade mb-16">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Transparent by Default</h2>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Every line of Toju's code is publicly available. You can read it, audit it, contribute to it, or fork it and build your own version. This
|
||||||
|
isn't a marketing decision - it's an accountability decision. When you can see exactly how the software works, you never have to take our
|
||||||
|
word for anything.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Open source also means Toju belongs to its community, not to a company. Even if we stopped development tomorrow, the project lives on. Your
|
||||||
|
communication infrastructure shouldn't depend on a single organization's survival.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commitment -->
|
||||||
|
<div class="section-fade rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 p-8 md:p-10">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6 !mt-0">Our Promise</h2>
|
||||||
|
<ul class="space-y-4 text-muted-foreground !list-none !pl-0">
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>We will <strong class="text-foreground">never</strong> lock features behind a paywall.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>We will <strong class="text-foreground">never</strong> sell, monetize, or harvest your data.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>We will <strong class="text-foreground">always</strong> keep the source code open and auditable.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>We will <strong class="text-foreground">always</strong> put users before profit.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-muted-foreground mt-6 text-sm">- The Myxelium team</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Support CTA -->
|
||||||
|
<section class="container mx-auto px-6">
|
||||||
|
<div class="section-fade max-w-2xl mx-auto text-center">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-4">Help us keep going</h2>
|
||||||
|
<p class="text-muted-foreground mb-8 leading-relaxed">
|
||||||
|
If Toju's mission resonates with you, consider supporting the project. Every contribution helps us keep the lights on and development moving
|
||||||
|
forward - without ever compromising our values.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/myxelium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-yellow-500 to-amber-500 text-black font-semibold hover:from-yellow-400 hover:to-amber-400 transition-all shadow-lg shadow-yellow-500/25"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/buymeacoffee.png"
|
||||||
|
alt=""
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
class="w-5 h-5 object-contain"
|
||||||
|
/>
|
||||||
|
Buy us a coffee
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Download Toju
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -16,160 +16,7 @@ import { AdSlotComponent } from '../../components/ad-slot/ad-slot.component';
|
|||||||
selector: 'app-philosophy',
|
selector: 'app-philosophy',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink, AdSlotComponent],
|
imports: [RouterLink, AdSlotComponent],
|
||||||
template: `
|
templateUrl: './philosophy.component.html'
|
||||||
<div class="min-h-screen pt-32 pb-20">
|
|
||||||
<!-- Hero -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<div class="max-w-3xl mx-auto text-center">
|
|
||||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6">
|
|
||||||
Our Manifesto
|
|
||||||
</div>
|
|
||||||
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">
|
|
||||||
Why we <span class="gradient-text">build</span> Toju
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-muted-foreground leading-relaxed">
|
|
||||||
A letter from the people behind the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<article class="max-w-3xl mx-auto prose prose-invert prose-lg">
|
|
||||||
<!-- Ownership -->
|
|
||||||
<div class="section-fade mb-16">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6 !mt-0">We Lost Something Important</h2>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Over the past two decades, something fundamental shifted. Our conversations, our memories, our connections -
|
|
||||||
they stopped belonging to us. They live on servers we don't control, inside apps that treat our personal lives
|
|
||||||
as data to be harvested, analyzed, and sold to the highest bidder.
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
We gave up ownership of our digital lives so gradually that most of us didn't even notice. A "free" app here,
|
|
||||||
a convenient service there - each one taking a little more of our privacy in exchange for convenience.
|
|
||||||
Toju exists because we believe it's time to take it back.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No predatory pricing -->
|
|
||||||
<div class="section-fade mb-16">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">No Paywalls. No Premium Tiers. Ever.</h2>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
You know the playbook: launch a free product, build a user base, then start locking features behind subscription
|
|
||||||
tiers. Can't share your screen at more than 720p unless you upgrade. File size limited to 8 MB. Want noise
|
|
||||||
suppression? That's a premium feature now.
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
We refuse to play that game. <strong class="text-foreground">Every feature in Toju is available to every user, always.</strong>
|
|
||||||
There is no "Toju Nitro," no "Pro plan," no artificial limitations designed to push you toward your wallet. Communication
|
|
||||||
is a human need, not a luxury - and the tools for it should reflect that.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Privacy as a right -->
|
|
||||||
<div class="section-fade mb-16">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Privacy Is a Right, Not a Feature</h2>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Most communication platforms collect everything: who you talk to, when, for how long, what you share. They build
|
|
||||||
profiles of your social graph, your habits, your interests. Even services that claim to care about privacy still
|
|
||||||
store metadata on their servers.
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Toju is architecturally different. Your data goes directly from your device to your friend's device. We don't have
|
|
||||||
your messages. We don't have your files. We don't have your call history. Not because we promised not to look -
|
|
||||||
but because the data never touches our infrastructure. We built the technology so that <strong class="text-foreground">privacy isn't
|
|
||||||
something we offer; it's something we literally cannot violate.</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Better world -->
|
|
||||||
<div class="section-fade mb-16">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Built from the Heart</h2>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Toju wasn't born in a boardroom with revenue projections. It was born from frustration - frustration with being
|
|
||||||
the product, with watching friends get locked out of features they used to have for free, with the growing feeling
|
|
||||||
that the tools we depend on daily don't actually serve our interests.
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
We build Toju because we genuinely want to make the world a little better. The internet was supposed to connect
|
|
||||||
people freely, and somewhere along the way, that mission got hijacked by business models that exploit the very
|
|
||||||
connections they facilitate. <strong class="text-foreground">Toju is our small act of reclaiming that original promise.</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Open source -->
|
|
||||||
<div class="section-fade mb-16">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6">Transparent by Default</h2>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Every line of Toju's code is publicly available. You can read it, audit it, contribute to it, or fork it and
|
|
||||||
build your own version. This isn't a marketing decision - it's an accountability decision. When you can see
|
|
||||||
exactly how the software works, you never have to take our word for anything.
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Open source also means Toju belongs to its community, not to a company. Even if we stopped development tomorrow,
|
|
||||||
the project lives on. Your communication infrastructure shouldn't depend on a single organization's survival.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Commitment -->
|
|
||||||
<div class="section-fade rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 p-8 md:p-10">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-6 !mt-0">Our Promise</h2>
|
|
||||||
<ul class="space-y-4 text-muted-foreground !list-none !pl-0">
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>We will <strong class="text-foreground">never</strong> lock features behind a paywall.</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>We will <strong class="text-foreground">never</strong> sell, monetize, or harvest your data.</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>We will <strong class="text-foreground">always</strong> keep the source code open and auditable.</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<svg class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
<span>We will <strong class="text-foreground">always</strong> put users before profit.</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-muted-foreground mt-6 text-sm">
|
|
||||||
- The Myxelium team
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Support CTA -->
|
|
||||||
<section class="container mx-auto px-6">
|
|
||||||
<div class="section-fade max-w-2xl mx-auto text-center">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-4">Help us keep going</h2>
|
|
||||||
<p class="text-muted-foreground mb-8 leading-relaxed">
|
|
||||||
If Toju's mission resonates with you, consider supporting the project. Every contribution helps us keep the
|
|
||||||
lights on and development moving forward - without ever compromising our values.
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
||||||
<a
|
|
||||||
href="https://buymeacoffee.com/myxelium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-yellow-500 to-amber-500 text-black font-semibold hover:from-yellow-400 hover:to-amber-400 transition-all shadow-lg shadow-yellow-500/25"
|
|
||||||
>
|
|
||||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.062 2.014.13l.04.005c.394.04.781.098 1.172.166.063.011.121.029.182.044.088.03.18.043.252.087a.556.556 0 01.272.476c.014.09.042.217.068.343.082.397.162.79.24 1.184.048.26.092.52.122.782.017.163.047.414-.076.556-.163.178-.49.132-.702.096a48.146 48.146 0 00-1.553-.22 30.38 30.38 0 00-3.346-.234c-1.108 0-2.217.066-3.315.2a25.855 25.855 0 00-1.578.244c-.156.03-.313.062-.469.097-.288.065-.564.195-.754.441-.131.17-.18.383-.134.596.046.213.167.396.34.525.294.215.626.358.976.454 1.015.285 2.087.38 3.136.397 1.21.018 2.42-.04 3.618-.177 1.004-.114 2-.3 2.95-.593.082-.025.165-.05.246-.078.224-.073.458-.142.676-.233.378-.157.727-.397.856-.795a1.38 1.38 0 00.048-.353v-.015c0-.049-.01-.096-.014-.144l-.018-.121c-.028-.179-.055-.362-.1-.539l-.025-.115-.038-.186c-.022-.11-.044-.22-.07-.327l-.061-.283c-.02-.096-.042-.19-.067-.284-.02-.074-.037-.15-.058-.224a2.98 2.98 0 00-.09-.3c-.034-.095-.078-.225-.148-.308z"/></svg>
|
|
||||||
Buy us a coffee
|
|
||||||
</a>
|
|
||||||
<a routerLink="/downloads" class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all">
|
|
||||||
Download Toju
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class PhilosophyComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class PhilosophyComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private readonly seoService = inject(SeoService);
|
private readonly seoService = inject(SeoService);
|
||||||
@@ -179,7 +26,9 @@ export class PhilosophyComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.update({
|
this.seoService.update({
|
||||||
title: 'Our Philosophy - Why We Build Toju',
|
title: 'Our Philosophy - Why We Build Toju',
|
||||||
description: 'Toju exists because privacy is a right, not a premium feature. No paywalls, no data harvesting, no predatory pricing. Learn why we build free, open-source communication tools.',
|
description:
|
||||||
|
'Toju exists because privacy is a right, not a premium feature. No paywalls, no data harvesting, no predatory '
|
||||||
|
+ 'pricing. Learn why we build free, open-source communication tools.',
|
||||||
url: 'https://toju.app/philosophy'
|
url: 'https://toju.app/philosophy'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
261
website/src/app/pages/what-is-toju/what-is-toju.component.html
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<div class="min-h-screen pt-32 pb-20">
|
||||||
|
<!-- Hero -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6"
|
||||||
|
>
|
||||||
|
The Big Picture
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">What is <span class="gradient-text">Toju</span>?</h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed">
|
||||||
|
Toju is a communication app that lets you voice chat, share your screen, send files, and message your friends - all without your data passing
|
||||||
|
through someone else's servers. Think of it as your own private phone line that nobody can tap into.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- How it works -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center section-fade">
|
||||||
|
How does it <span class="gradient-text">work</span>?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid gap-8">
|
||||||
|
<!-- Step 1 -->
|
||||||
|
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
||||||
|
<div class="flex items-start gap-6">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-12 h-12 rounded-full bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 font-bold text-lg"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">You connect directly to your friends</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
When you start a call or send a file on Toju, your data travels directly from your device to your friend's device. There's no company
|
||||||
|
server in the middle storing your conversations, listening to your calls, or scanning your files. This is called
|
||||||
|
<strong class="text-foreground">peer-to-peer</strong> - it's like having a direct road between your houses instead of going through a
|
||||||
|
toll booth.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2 -->
|
||||||
|
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
||||||
|
<div class="flex items-start gap-6">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-12 h-12 rounded-full bg-violet-500/10 border border-violet-500/20 flex items-center justify-center text-violet-400 font-bold text-lg"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">A tiny helper gets you connected</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
The only thing a server does is help your device find your friend's device - like a mutual friend introducing you at a party. Once
|
||||||
|
you're connected, the server steps out of the picture entirely. It never sees what you say, share, or send. This helper is called a
|
||||||
|
<strong class="text-foreground">signal server</strong>, and you can even run your own if you'd like.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3 -->
|
||||||
|
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
||||||
|
<div class="flex items-start gap-6">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-12 h-12 rounded-full bg-pink-500/10 border border-pink-500/20 flex items-center justify-center text-pink-400 font-bold text-lg"
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-foreground mb-3">No limits because there are no middlemen</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
|
Since your data doesn't pass through our servers, we don't need to pay for massive infrastructure. That's why Toju can offer
|
||||||
|
<strong class="text-foreground">unlimited screen sharing, file transfers of any size, and high-quality voice</strong> - all completely
|
||||||
|
free. There's no business reason to limit what you can do, and we never will.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-ad-slot />
|
||||||
|
|
||||||
|
<!-- Why designed this way -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center section-fade">
|
||||||
|
Why is it <span class="gradient-text">designed</span> this way?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 gap-8">
|
||||||
|
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-emerald-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-3">Privacy by Architecture</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
We didn't just add privacy as a feature - we built the entire app around it. When there's no central server handling your data, there's
|
||||||
|
nothing to hack, subpoena, or sell. Your privacy isn't protected by a promise; it's protected by how the technology works.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-blue-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-3">Performance Without Compromise</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
Direct connections mean lower latency. Your voice reaches your friend faster. Your screen share is smoother. Your file arrives in the time
|
||||||
|
it actually takes to transfer - not in the time it takes to upload, store, then download.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-amber-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-3">Sustainable & Free</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
Running a traditional chat service with millions of users costs enormous amounts of money for servers. That cost gets passed to you. With
|
||||||
|
peer-to-peer, the only infrastructure we run is a tiny coordination server - costing almost nothing. That's how we keep it free
|
||||||
|
permanently.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-purple-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-3">Independence & Freedom</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
You're not locked into our ecosystem. The code is open source. You can run your own server. If we ever disappeared tomorrow, you could
|
||||||
|
still use Toju. Your communication tools should belong to you, not a corporation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FAQ-style section -->
|
||||||
|
<section class="container mx-auto px-6 mb-24">
|
||||||
|
<div class="max-w-3xl mx-auto section-fade">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center">Common <span class="gradient-text">Questions</span></h2>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-2">Is Toju really free? What's the catch?</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
Yes, it's really free. There is no catch. Because Toju uses peer-to-peer connections, we don't need expensive server infrastructure. Our
|
||||||
|
costs are minimal, and we fund development through community support and donations. Every feature is available to everyone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-2">Do I need technical knowledge to use Toju?</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
Not at all. Toju works like any other chat app - download it, create an account, and start talking. All the peer-to-peer magic happens
|
||||||
|
behind the scenes. You just enjoy the benefits of better privacy, performance, and no limits.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-2">What does "self-host the signal server" mean?</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
The signal server is a tiny program that helps users find each other online. We run one by default, but if you prefer complete control,
|
||||||
|
you can run your own copy on your own hardware. It's like having your own private phone directory - only people you invite can use it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground mb-2">Is my data safe?</h3>
|
||||||
|
<p class="text-muted-foreground leading-relaxed text-sm">
|
||||||
|
Your conversations, files, and calls go directly between you and the person you're talking to. They never pass through or get stored on
|
||||||
|
our servers. Even if someone broke into our server, there would be nothing to find - because we never had your data in the first place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<section class="container mx-auto px-6">
|
||||||
|
<div
|
||||||
|
class="section-fade max-w-2xl mx-auto text-center rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 p-12"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-4">Ready to try it?</h2>
|
||||||
|
<p class="text-muted-foreground mb-8">Available on Windows, Linux, and in your browser. Always free.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
routerLink="/downloads"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25"
|
||||||
|
>
|
||||||
|
Download Toju
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://web.toju.app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Open in Browser
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -16,202 +16,7 @@ import { AdSlotComponent } from '../../components/ad-slot/ad-slot.component';
|
|||||||
selector: 'app-what-is-toju',
|
selector: 'app-what-is-toju',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink, AdSlotComponent],
|
imports: [RouterLink, AdSlotComponent],
|
||||||
template: `
|
templateUrl: './what-is-toju.component.html'
|
||||||
<div class="min-h-screen pt-32 pb-20">
|
|
||||||
<!-- Hero -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<div class="max-w-3xl mx-auto text-center">
|
|
||||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6">
|
|
||||||
The Big Picture
|
|
||||||
</div>
|
|
||||||
<h1 class="text-4xl md:text-6xl font-extrabold text-foreground mb-6">
|
|
||||||
What is <span class="gradient-text">Toju</span>?
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-muted-foreground leading-relaxed">
|
|
||||||
Toju is a communication app that lets you voice chat, share your screen, send files, and message your friends -
|
|
||||||
all without your data passing through someone else's servers. Think of it as your own private phone line that nobody can tap into.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- How it works -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center section-fade">
|
|
||||||
How does it <span class="gradient-text">work</span>?
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="grid gap-8">
|
|
||||||
<!-- Step 1 -->
|
|
||||||
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
|
||||||
<div class="flex items-start gap-6">
|
|
||||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 font-bold text-lg">1</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">You connect directly to your friends</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
When you start a call or send a file on Toju, your data travels directly from your device to your friend's device.
|
|
||||||
There's no company server in the middle storing your conversations, listening to your calls, or scanning your files.
|
|
||||||
This is called <strong class="text-foreground">peer-to-peer</strong> - it's like having a direct road between your houses instead of going through a toll booth.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 2 -->
|
|
||||||
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
|
||||||
<div class="flex items-start gap-6">
|
|
||||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-violet-500/10 border border-violet-500/20 flex items-center justify-center text-violet-400 font-bold text-lg">2</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">A tiny helper gets you connected</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
The only thing a server does is help your device find your friend's device - like a mutual friend introducing you at a party.
|
|
||||||
Once you're connected, the server steps out of the picture entirely. It never sees what you say, share, or send.
|
|
||||||
This helper is called a <strong class="text-foreground">signal server</strong>, and you can even run your own if you'd like.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 3 -->
|
|
||||||
<div class="section-fade relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 md:p-10">
|
|
||||||
<div class="flex items-start gap-6">
|
|
||||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-pink-500/10 border border-pink-500/20 flex items-center justify-center text-pink-400 font-bold text-lg">3</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-semibold text-foreground mb-3">No limits because there are no middlemen</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
|
||||||
Since your data doesn't pass through our servers, we don't need to pay for massive infrastructure.
|
|
||||||
That's why Toju can offer <strong class="text-foreground">unlimited screen sharing, file transfers of any size, and high-quality voice</strong> - all completely free.
|
|
||||||
There's no business reason to limit what you can do, and we never will.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<app-ad-slot />
|
|
||||||
|
|
||||||
<!-- Why designed this way -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center section-fade">
|
|
||||||
Why is it <span class="gradient-text">designed</span> this way?
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-8">
|
|
||||||
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center mb-4">
|
|
||||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-3">Privacy by Architecture</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
We didn't just add privacy as a feature - we built the entire app around it. When there's no central server handling your data,
|
|
||||||
there's nothing to hack, subpoena, or sell. Your privacy isn't protected by a promise; it's protected by how the technology works.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center mb-4">
|
|
||||||
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-3">Performance Without Compromise</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
Direct connections mean lower latency. Your voice reaches your friend faster. Your screen share is smoother.
|
|
||||||
Your file arrives in the time it actually takes to transfer - not in the time it takes to upload, store, then download.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center mb-4">
|
|
||||||
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-3">Sustainable & Free</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
Running a traditional chat service with millions of users costs enormous amounts of money for servers. That cost gets passed to you.
|
|
||||||
With peer-to-peer, the only infrastructure we run is a tiny coordination server - costing almost nothing. That's how we keep it free permanently.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-fade rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center mb-4">
|
|
||||||
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-3">Independence & Freedom</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
You're not locked into our ecosystem. The code is open source. You can run your own server.
|
|
||||||
If we ever disappeared tomorrow, you could still use Toju. Your communication tools should belong to you, not a corporation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FAQ-style section -->
|
|
||||||
<section class="container mx-auto px-6 mb-24">
|
|
||||||
<div class="max-w-3xl mx-auto section-fade">
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-12 text-center">
|
|
||||||
Common <span class="gradient-text">Questions</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-2">Is Toju really free? What's the catch?</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
Yes, it's really free. There is no catch. Because Toju uses peer-to-peer connections, we don't need expensive server
|
|
||||||
infrastructure. Our costs are minimal, and we fund development through community support and donations. Every feature
|
|
||||||
is available to everyone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-2">Do I need technical knowledge to use Toju?</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
Not at all. Toju works like any other chat app - download it, create an account, and start talking. All the
|
|
||||||
peer-to-peer magic happens behind the scenes. You just enjoy the benefits of better privacy, performance, and no limits.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-2">What does "self-host the signal server" mean?</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
The signal server is a tiny program that helps users find each other online. We run one by default, but if you
|
|
||||||
prefer complete control, you can run your own copy on your own hardware. It's like having your own private phone
|
|
||||||
directory - only people you invite can use it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-6 md:p-8">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground mb-2">Is my data safe?</h3>
|
|
||||||
<p class="text-muted-foreground leading-relaxed text-sm">
|
|
||||||
Your conversations, files, and calls go directly between you and the person you're talking to. They never pass through
|
|
||||||
or get stored on our servers. Even if someone broke into our server, there would be nothing to find - because we never
|
|
||||||
had your data in the first place.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<section class="container mx-auto px-6">
|
|
||||||
<div class="section-fade max-w-2xl mx-auto text-center rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-950/20 to-violet-950/20 p-12">
|
|
||||||
<h2 class="text-2xl md:text-3xl font-bold text-foreground mb-4">Ready to try it?</h2>
|
|
||||||
<p class="text-muted-foreground mb-8">Available on Windows, Linux, and in your browser. Always free.</p>
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
||||||
<a routerLink="/downloads" class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25">
|
|
||||||
Download Toju
|
|
||||||
</a>
|
|
||||||
<a href="https://web.toju.app/" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all">
|
|
||||||
Open in Browser
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class WhatIsTojuComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class WhatIsTojuComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private readonly seoService = inject(SeoService);
|
private readonly seoService = inject(SeoService);
|
||||||
@@ -221,7 +26,9 @@ export class WhatIsTojuComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.update({
|
this.seoService.update({
|
||||||
title: 'What is Toju? - How It Works',
|
title: 'What is Toju? - How It Works',
|
||||||
description: 'Learn how Toju\'s peer-to-peer technology delivers free, private, unlimited voice calls, screen sharing, and file transfers without centralized servers.',
|
description:
|
||||||
|
'Learn how Toju\'s peer-to-peer technology delivers free, private, unlimited voice calls, screen sharing, and '
|
||||||
|
+ 'file transfers without centralized servers.',
|
||||||
url: 'https://toju.app/what-is-toju'
|
url: 'https://toju.app/what-is-toju'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
inject,
|
PLATFORM_ID,
|
||||||
PLATFORM_ID
|
inject
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
||||||
|
|
||||||
@@ -27,6 +27,45 @@ export interface DetectedOS {
|
|||||||
ymlFile: string;
|
ymlFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WINDOWS_SUFFIXES = ['.exe', '.msi'];
|
||||||
|
const WINDOWS_HINTS = [
|
||||||
|
'setup',
|
||||||
|
'win',
|
||||||
|
'windows'
|
||||||
|
];
|
||||||
|
const MAC_SUFFIXES = ['.dmg', '.pkg'];
|
||||||
|
const MAC_HINTS = [
|
||||||
|
'mac',
|
||||||
|
'macos',
|
||||||
|
'osx',
|
||||||
|
'darwin'
|
||||||
|
];
|
||||||
|
const LINUX_SUFFIXES = [
|
||||||
|
'.appimage',
|
||||||
|
'.deb',
|
||||||
|
'.rpm'
|
||||||
|
];
|
||||||
|
const LINUX_HINTS = ['linux'];
|
||||||
|
const ARCHIVE_SUFFIXES = [
|
||||||
|
'.zip',
|
||||||
|
'.tar',
|
||||||
|
'.tar.gz',
|
||||||
|
'.tgz',
|
||||||
|
'.tar.xz',
|
||||||
|
'.7z',
|
||||||
|
'.rar'
|
||||||
|
];
|
||||||
|
|
||||||
|
function matchesAssetPattern(name: string, suffixes: string[], hints: string[] = []): boolean {
|
||||||
|
if (suffixes.some((suffix) => name.endsWith(suffix))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = name.split(/[^a-z0-9]+/).filter(Boolean);
|
||||||
|
|
||||||
|
return hints.some((hint) => tokens.includes(hint));
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ReleaseService {
|
export class ReleaseService {
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
@@ -36,65 +75,69 @@ export class ReleaseService {
|
|||||||
|
|
||||||
detectOS(): DetectedOS {
|
detectOS(): DetectedOS {
|
||||||
if (!isPlatformBrowser(this.platformId)) {
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
return { name: 'Linux', icon: null, filePattern: /\.AppImage$/i, ymlFile: 'latest-linux.yml' };
|
return {
|
||||||
|
name: 'Linux',
|
||||||
|
icon: null,
|
||||||
|
filePattern: /\.AppImage$/i,
|
||||||
|
ymlFile: 'latest-linux.yml'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
|
||||||
if (ua.includes('win')) {
|
if (userAgent.includes('win')) {
|
||||||
return { name: 'Windows', icon: null, filePattern: /\.exe$/i, ymlFile: 'latest.yml' };
|
return {
|
||||||
|
name: 'Windows',
|
||||||
|
icon: null,
|
||||||
|
filePattern: /\.exe$/i,
|
||||||
|
ymlFile: 'latest.yml'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ua.includes('mac')) {
|
if (userAgent.includes('mac')) {
|
||||||
return { name: 'macOS', icon: null, filePattern: /\.dmg$/i, ymlFile: 'latest-mac.yml' };
|
return {
|
||||||
|
name: 'macOS',
|
||||||
|
icon: null,
|
||||||
|
filePattern: /\.dmg$/i,
|
||||||
|
ymlFile: 'latest-mac.yml'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux - prefer .deb for Ubuntu/Debian-based distros, otherwise AppImage
|
const isUbuntuDebian = userAgent.includes('ubuntu') || userAgent.includes('debian');
|
||||||
const isUbuntuDebian = ua.includes('ubuntu') || ua.includes('debian');
|
|
||||||
|
|
||||||
if (isUbuntuDebian) {
|
if (isUbuntuDebian) {
|
||||||
return { name: 'Linux (deb)', icon: null, filePattern: /\.deb$/i, ymlFile: 'latest-linux.yml' };
|
return {
|
||||||
|
name: 'Linux (deb)',
|
||||||
|
icon: null,
|
||||||
|
filePattern: /\.deb$/i,
|
||||||
|
ymlFile: 'latest-linux.yml'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name: 'Linux', icon: null, filePattern: /\.AppImage$/i, ymlFile: 'latest-linux.yml' };
|
return {
|
||||||
|
name: 'Linux',
|
||||||
|
icon: null,
|
||||||
|
filePattern: /\.AppImage$/i,
|
||||||
|
ymlFile: 'latest-linux.yml'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchReleases(): Promise<Release[]> {
|
fetchReleases(): Promise<Release[]> {
|
||||||
if (this.cachedReleases)
|
if (this.cachedReleases) {
|
||||||
return Promise.resolve(this.cachedReleases);
|
return Promise.resolve(this.cachedReleases);
|
||||||
|
|
||||||
// Skip during SSR to avoid fetch errors during prerendering
|
|
||||||
if (isPlatformServer(this.platformId))
|
|
||||||
return Promise.resolve([]);
|
|
||||||
|
|
||||||
// Deduplicate concurrent calls
|
|
||||||
if (this.fetchPromise)
|
|
||||||
return this.fetchPromise;
|
|
||||||
|
|
||||||
this.fetchPromise = this._doFetch();
|
|
||||||
return this.fetchPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _doFetch(): Promise<Release[]> {
|
|
||||||
try {
|
|
||||||
// Use our SSR proxy to avoid CORS issues with the Gitea API
|
|
||||||
const response = await fetch('/api/releases', {
|
|
||||||
headers: { 'Accept': 'application/json' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
this.cachedReleases = Array.isArray(data) ? data : [data];
|
|
||||||
return this.cachedReleases;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch releases:', err);
|
|
||||||
return [];
|
|
||||||
} finally {
|
|
||||||
this.fetchPromise = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPlatformServer(this.platformId)) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fetchPromise) {
|
||||||
|
return this.fetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchPromise = this.fetchReleasesInternal();
|
||||||
|
|
||||||
|
return this.fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestRelease(): Promise<Release | null> {
|
async getLatestRelease(): Promise<Release | null> {
|
||||||
@@ -106,42 +149,81 @@ export class ReleaseService {
|
|||||||
async getDownloadUrl(os: DetectedOS): Promise<string | null> {
|
async getDownloadUrl(os: DetectedOS): Promise<string | null> {
|
||||||
const release = await this.getLatestRelease();
|
const release = await this.getLatestRelease();
|
||||||
|
|
||||||
if (!release)
|
if (!release) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const asset = release.assets.find(a => os.filePattern.test(a.name));
|
const matchingAsset = release.assets.find(
|
||||||
|
(releaseAsset) => os.filePattern.test(releaseAsset.name)
|
||||||
|
);
|
||||||
|
|
||||||
return asset?.browser_download_url ?? null;
|
return matchingAsset?.browser_download_url ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetOS(name: string): string {
|
getAssetOS(name: string): string {
|
||||||
const lower = name.toLowerCase();
|
const lower = name.toLowerCase();
|
||||||
|
|
||||||
if (lower.endsWith('.exe') || lower.includes('setup') || lower.endsWith('.msi'))
|
if (matchesAssetPattern(lower, WINDOWS_SUFFIXES, WINDOWS_HINTS)) {
|
||||||
return 'Windows';
|
return 'Windows';
|
||||||
|
}
|
||||||
|
|
||||||
if (lower.endsWith('.dmg') || lower.includes('mac'))
|
if (matchesAssetPattern(lower, MAC_SUFFIXES, MAC_HINTS)) {
|
||||||
return 'macOS';
|
return 'macOS';
|
||||||
|
}
|
||||||
|
|
||||||
if (lower.endsWith('.appimage') || lower.endsWith('.deb') || lower.endsWith('.rpm') || lower.includes('linux'))
|
if (matchesAssetPattern(lower, LINUX_SUFFIXES, LINUX_HINTS)) {
|
||||||
return 'Linux';
|
return 'Linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesAssetPattern(lower, ARCHIVE_SUFFIXES)) {
|
||||||
|
return 'Archive';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.endsWith('.wasm')) {
|
||||||
|
return 'Web';
|
||||||
|
}
|
||||||
|
|
||||||
return 'Other';
|
return 'Other';
|
||||||
}
|
}
|
||||||
|
|
||||||
formatBytes(bytes: number): string {
|
formatBytes(bytes: number): string {
|
||||||
if (bytes === 0)
|
if (bytes === 0) {
|
||||||
return '0 B';
|
return '0 B';
|
||||||
|
}
|
||||||
|
|
||||||
const k = 1024;
|
const kilobyte = 1024;
|
||||||
const sizes = [
|
const units = [
|
||||||
'B',
|
'B',
|
||||||
'KB',
|
'KB',
|
||||||
'MB',
|
'MB',
|
||||||
'GB'
|
'GB'
|
||||||
];
|
];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const unitIndex = Math.floor(Math.log(bytes) / Math.log(kilobyte));
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / Math.pow(kilobyte, unitIndex)).toFixed(1))} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchReleasesInternal(): Promise<Release[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/releases', {
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.cachedReleases = Array.isArray(data) ? data : [data];
|
||||||
|
|
||||||
|
return this.cachedReleases;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch releases:', error);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
this.fetchPromise = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import {
|
import { Injectable, inject } from '@angular/core';
|
||||||
Injectable,
|
|
||||||
inject,
|
|
||||||
PLATFORM_ID
|
|
||||||
} from '@angular/core';
|
|
||||||
import { Meta, Title } from '@angular/platform-browser';
|
import { Meta, Title } from '@angular/platform-browser';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
|
||||||
|
|
||||||
interface SeoData {
|
interface SeoData {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
BIN
website/src/images/buymeacoffee.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
website/src/images/gitea.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
website/src/images/github.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
website/src/images/misc/file.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
website/src/images/misc/zip.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
website/src/images/screenshots/filedownload.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
website/src/images/screenshots/gif.png
Normal file
|
After Width: | Height: | Size: 505 KiB |
BIN
website/src/images/screenshots/music.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
website/src/images/screenshots/screenshare_gaming.png
Normal file
|
After Width: | Height: | Size: 816 KiB |
BIN
website/src/images/screenshots/screenshot_main.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
website/src/images/screenshots/serverViewScreen.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
website/src/images/screenshots/videos.png
Normal file
|
After Width: | Height: | Size: 549 KiB |
BIN
website/src/images/toju-logo-transparent.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
@@ -2,7 +2,6 @@ import { BootstrapContext, bootstrapApplication } from '@angular/platform-browse
|
|||||||
import { AppComponent } from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
import { config } from './app/app.config.server';
|
import { config } from './app/app.config.server';
|
||||||
|
|
||||||
const bootstrap = (context: BootstrapContext) =>
|
const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
|
||||||
bootstrapApplication(AppComponent, config, context);
|
|
||||||
|
|
||||||
export default bootstrap;
|
export default bootstrap;
|
||||||
|
|||||||
@@ -1,54 +1,70 @@
|
|||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import { CommonEngine, isMainModule } from '@angular/ssr/node';
|
import { CommonEngine, isMainModule } from '@angular/ssr/node';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import {
|
||||||
|
dirname,
|
||||||
|
join,
|
||||||
|
resolve
|
||||||
|
} from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import bootstrap from './main.server';
|
import bootstrap from './main.server';
|
||||||
|
|
||||||
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
||||||
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
||||||
const indexHtml = join(serverDistFolder, 'index.server.html');
|
const indexHtml = join(serverDistFolder, 'index.server.html');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const commonEngine = new CommonEngine();
|
const commonEngine = new CommonEngine();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxy endpoint for Gitea releases API to avoid CORS issues.
|
* Proxy endpoint for Gitea releases API to avoid CORS issues.
|
||||||
*/
|
*/
|
||||||
app.get('/api/releases', async (req, res) => {
|
app.get('/api/releases', async (_request, response) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://git.azaaxin.com/api/v1/repos/myxelium/Toju/releases', {
|
const upstreamResponse = await fetch('https://git.azaaxin.com/api/v1/repos/myxelium/Toju/releases', {
|
||||||
headers: { 'Accept': 'application/json' },
|
headers: { Accept: 'application/json' }
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
res.status(response.status).json({ error: `Upstream returned ${response.status}` });
|
if (!upstreamResponse.ok) {
|
||||||
|
response.status(upstreamResponse.status).json({
|
||||||
|
error: `Upstream returned ${upstreamResponse.status}`
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
|
||||||
res.setHeader('Cache-Control', 'public, max-age=300'); // cache 5 min
|
const data = await upstreamResponse.json();
|
||||||
res.json(data);
|
|
||||||
} catch (err) {
|
response.setHeader('Cache-Control', 'public, max-age=300');
|
||||||
console.error('Proxy fetch error:', err);
|
response.json(data);
|
||||||
res.status(502).json({ error: 'Failed to fetch releases from upstream' });
|
} catch (error) {
|
||||||
|
console.error('Proxy fetch error:', error);
|
||||||
|
response.status(502).json({
|
||||||
|
error: 'Failed to fetch releases from upstream'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve static files from /browser
|
* Serve static files from /browser.
|
||||||
*/
|
*/
|
||||||
app.get(
|
app.get(
|
||||||
'**',
|
'**',
|
||||||
express.static(browserDistFolder, {
|
express.static(browserDistFolder, {
|
||||||
maxAge: '1y',
|
maxAge: '1y',
|
||||||
index: 'index.html'
|
index: 'index.html'
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle all other requests by rendering the Angular application.
|
* Handle all other requests by rendering the Angular application.
|
||||||
*/
|
*/
|
||||||
app.get('**', (req, res, next) => {
|
app.get('**', (request, response, next) => {
|
||||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
const {
|
||||||
|
protocol,
|
||||||
|
originalUrl,
|
||||||
|
baseUrl,
|
||||||
|
headers
|
||||||
|
} = request;
|
||||||
|
|
||||||
commonEngine
|
commonEngine
|
||||||
.render({
|
.render({
|
||||||
@@ -56,18 +72,18 @@ app.get('**', (req, res, next) => {
|
|||||||
documentFilePath: indexHtml,
|
documentFilePath: indexHtml,
|
||||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||||
publicPath: browserDistFolder,
|
publicPath: browserDistFolder,
|
||||||
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }]
|
||||||
})
|
})
|
||||||
.then((html) => res.send(html))
|
.then((html) => response.send(html))
|
||||||
.catch((err) => next(err));
|
.catch((error) => next(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the server if this module is the main entry point.
|
* Start the server if this module is the main entry point.
|
||||||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
|
|
||||||
*/
|
*/
|
||||||
if (isMainModule(import.meta.url)) {
|
if (isMainModule(import.meta.url)) {
|
||||||
const port = process.env['PORT'] || 4000;
|
const port = process.env['PORT'] || 4000;
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Node Express server listening on http://localhost:${port}`);
|
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|||||||