* [PATCH v1 01/13] fix(toaster): add Toaster component
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 02/13] chore(login): remove unnecessary default data Muhammad Rizki
` (13 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added the Toaster component to the main layout.
Note:
I didn't realize that I had forgotten to add the Toaster component
for displaying toast messages.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/+layout.svelte | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 801be90..e0aab7c 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,7 +1,10 @@
<script lang="ts">
+ import { Toaster } from "$components/ui/sonner";
import "../app.css";
let { children } = $props();
</script>
{@render children()}
+
+<Toaster />
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 02/13] chore(login): remove unnecessary default data
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 01/13] fix(toaster): add Toaster component Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 03/13] feat(constants): add settingsNav data Muhammad Rizki
` (12 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/+page.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/routes/+page.ts b/src/routes/+page.ts
index 99ac97a..c3ac27a 100644
--- a/src/routes/+page.ts
+++ b/src/routes/+page.ts
@@ -7,11 +7,8 @@ import { redirect } from "@sveltejs/kit";
export const load: PageLoad = async () => {
const auth = useAuth();
-
if (auth.isValid()) return redirect(307, "/home");
- const data = { email: "", password: "" };
- const form = await superValidate(data, zod(loginSchema));
-
+ const form = await superValidate(zod(loginSchema));
return { form };
};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 03/13] feat(constants): add settingsNav data
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 01/13] fix(toaster): add Toaster component Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 02/13] chore(login): remove unnecessary default data Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 04/13] feat(typings/common): add disabled property for Navigations Muhammad Rizki
` (11 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
This constant stores navigation data for the settings page.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/constants/navigations.ts | 13 +++++++++++++
src/lib/typings/common.d.ts | 1 +
2 files changed, 14 insertions(+)
diff --git a/src/lib/constants/navigations.ts b/src/lib/constants/navigations.ts
index fc626aa..ada0e40 100644
--- a/src/lib/constants/navigations.ts
+++ b/src/lib/constants/navigations.ts
@@ -13,3 +13,16 @@ export const navigations: typing.Navigations[] = [
url: "/settings"
}
] as const;
+
+export const settingsNav: typing.Navigations[] = [
+ {
+ name: "Profile",
+ description: "Manage your profile.",
+ url: "/settings/profile"
+ },
+ {
+ name: "Account",
+ description: "Manage your account credentials.",
+ url: "/settings/account"
+ }
+];
diff --git a/src/lib/typings/common.d.ts b/src/lib/typings/common.d.ts
index e2c0164..fa3b8ca 100644
--- a/src/lib/typings/common.d.ts
+++ b/src/lib/typings/common.d.ts
@@ -4,6 +4,7 @@ export type RecordString = Record<string, string>;
export interface Navigations {
name: string;
+ description?: string;
icon?: typeof IconType;
url: string;
}
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 04/13] feat(typings/common): add disabled property for Navigations
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (2 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 03/13] feat(constants): add settingsNav data Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 05/13] refactor: update HTTP client, typings, and login method Muhammad Rizki
` (10 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added the `disabled` property to make some navigation routes
unclickable.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/typings/common.d.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/lib/typings/common.d.ts b/src/lib/typings/common.d.ts
index fa3b8ca..cdbe0d4 100644
--- a/src/lib/typings/common.d.ts
+++ b/src/lib/typings/common.d.ts
@@ -7,6 +7,7 @@ export interface Navigations {
description?: string;
icon?: typeof IconType;
url: string;
+ disabled?: boolean;
}
export interface LabelAndValue {
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 05/13] refactor: update HTTP client, typings, and login method
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (3 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 04/13] feat(typings/common): add disabled property for Navigations Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 06/13] chore(schema): rename login schema Muhammad Rizki
` (9 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Refactored the HTTP client, typings, and login query method. The new
method is much simpler than previous one. Additionally, the refactored
HTTP client now includes refresh token behavior under the hood.
Added an invalid credential warning message to the login form.
When a user accesses the page with an expired token, they will be
redirected to the login page and shown a message indicating that the
token has expired and they need to re-login.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/hooks/http.svelte.ts | 170 ++++++++++--------------------
src/lib/typings/http.d.ts | 22 ++--
src/lib/typings/index.ts | 5 +-
src/routes/(protected)/+layout.ts | 14 ++-
src/routes/+page.svelte | 48 +++++----
src/routes/+page.ts | 4 +-
6 files changed, 103 insertions(+), 160 deletions(-)
diff --git a/src/lib/hooks/http.svelte.ts b/src/lib/hooks/http.svelte.ts
index fd4114d..78273a1 100644
--- a/src/lib/hooks/http.svelte.ts
+++ b/src/lib/hooks/http.svelte.ts
@@ -1,129 +1,65 @@
-import axios, { AxiosError } from "axios";
-import { useAuth } from "./auth.svelte";
-import type { RecordString, UseHttpProps } from "$typings";
-import type { ResponseAPI } from "$typings/http";
-
-const transformToFormData = (initialData: RecordString, requestData: RecordString | FormData) => {
- const formData = new FormData();
-
- let data = requestData;
-
- Object.keys(initialData).forEach((key) => {
- const value = initialData[key];
+import * as typing from "$typings";
+import axios from "axios";
+import type {
+ AxiosError,
+ AxiosRequestConfig,
+ AxiosResponse,
+ InternalAxiosRequestConfig
+} from "axios";
+
+const client = axios.create({
+ baseURL: "https://mail.gnuweeb.org/api.php"
+});
+
+const http = async <T>(prop: AxiosRequestConfig) => {
+ const onSuccess = (response: AxiosResponse<typing.ResponseAPI<T>>) => {
+ return response;
+ };
- if (Array.isArray(value)) {
- for (const val of value) {
- formData.append(
- key,
- typeof val === "object" && !(val instanceof File) ? JSON.stringify(val) : val
- );
- }
- } else {
- formData.append(key, value);
- }
- });
+ const onError = (error: AxiosError) => {
+ return Promise.reject(error);
+ };
- data = formData;
- return data;
+ return client(prop).then(onSuccess).catch(onError);
};
-const GWM_API_URL = "https://mail.gnuweeb.org/api.php?action=";
+client.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ const accessToken = localStorage.getItem("gwm_token");
-export const useHttp = <Response = unknown, Formatter = unknown>({
- action,
- payload,
- params = {},
- method = "GET",
- timeout,
- beforeExecute = () => true,
- onComplete = () => true,
- formatter = () => true,
- onError = () => false,
- onFinally = () => false,
- isFormData = false,
- responseType
-}: UseHttpProps<Response, Formatter>) => {
- const url = GWM_API_URL + action;
- let isLoading = $state<boolean>(true);
- let data = $state<Formatter | null>(null);
+ config.params = { ...config.params, renew_token: 1 };
- formatter(data, null);
-
- let errors = $state<string | null>(null);
- let errorMessage = $state<string | null>(null);
- const auth = useAuth();
-
- const execute = async (replace?: {
- payload?: RecordString | null;
- params?: RecordString | null;
- }) => {
- if (!beforeExecute()) {
- onFinally();
- return;
+ if (accessToken && !config.headers.Authorization) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
}
- isLoading = true;
-
- if (replace?.payload?.defaultPrevented != null) {
- replace!.payload = null;
+ return config;
+ },
+ (error: AxiosError) => {
+ return Promise.reject(error);
+ }
+);
+
+client.interceptors.response.use(
+ (res: AxiosResponse) => {
+ if (res?.data.res?.renew_token) {
+ localStorage.setItem("gwm_token", res.data.res.renew_token.token);
+ localStorage.setItem("gwm_token_exp_at", res.data.res.renew_token.token_exp_at);
}
-
- const initialData: RecordString = replace?.payload ?? payload ?? {};
- let requestData: RecordString | FormData = initialData;
-
- if (isFormData) {
- requestData = transformToFormData(initialData, requestData);
+ return res;
+ },
+ async (err: AxiosError) => {
+ const response = err.response as AxiosResponse<typing.ResponseAPI<typing.RenewTokenResponse>>;
+ const status = response ? response.status : null;
+
+ if (status === 403 && response?.data) {
+ localStorage.removeItem("gwm_token");
+ localStorage.removeItem("gwm_token_exp_at");
+ localStorage.removeItem("gwm_uinfo");
}
- return await axios({
- url: url,
- method,
- data: requestData,
- headers: {
- Authorization: `Bearer ${auth.token}`,
- "Content-Type": "application/json"
- },
- params: replace?.params ?? params,
- responseType: responseType,
- timeout: timeout ?? 60000
- })
- .then((res) => {
- isLoading = false;
- data = res?.data;
-
- onComplete(res);
- onFinally();
- formatter(data, res);
+ return response;
+ }
+);
- errors = null;
- errorMessage = "";
-
- return res;
- })
-
- .catch((res: AxiosError<ResponseAPI<Response>>) => {
- isLoading = false;
- errors = res.stack!;
-
- const response = res.response?.data;
-
- errorMessage = typeof response?.res === "string" ? response.res : "Something went wrong!";
-
- onError(res.response!.data, errorMessage);
- onFinally();
-
- return res;
- });
- };
-
- const refetch = execute;
-
- return {
- data,
- isLoading,
- refetch,
- execute,
- errorMessage,
- errors
- };
-};
+export default http;
diff --git a/src/lib/typings/http.d.ts b/src/lib/typings/http.d.ts
index decf147..98d64f5 100644
--- a/src/lib/typings/http.d.ts
+++ b/src/lib/typings/http.d.ts
@@ -1,24 +1,14 @@
-import type { AxiosRequestConfig, AxiosResponse } from "axios";
-import type { RecordString } from "./common";
import type { User } from "./credential";
-interface ResponseInterface<Data> {
+export interface ResponseAPI<Data> {
code: number;
- res?: Data;
+ res?: Data & { renew_token?: RenewTokenResponse };
}
-type ResponseAPI<ArrayType = null> = ResponseInterface<ArrayType>;
-
-export type UseHttpProps<Response, Formatter> = AxiosRequestConfig & {
- action: string;
- payload?: RecordString;
- isFormData?: boolean;
- beforeExecute?: () => boolean;
- onComplete?: (resp: AxiosResponse<ResponseAPI<Response>>) => void;
- formatter?: (data: Formatter | null, resp: AxiosResponse<ResponseAPI<Response>> | null) => void;
- onError?: (data: ResponseAPI<Response>, msg?: string | null) => void;
- onFinally?: () => void;
-};
+export interface RenewTokenResponse {
+ token: strign;
+ token_exp_at: number;
+}
export interface LoginResponse {
token: string;
diff --git a/src/lib/typings/index.ts b/src/lib/typings/index.ts
index 6ae23a1..1855c2b 100644
--- a/src/lib/typings/index.ts
+++ b/src/lib/typings/index.ts
@@ -1,5 +1,5 @@
import type { RecordString, Navigations, LabelAndValue, MailConfig } from "./common";
-import type { UseHttpProps, LoginResponse } from "./http";
+import type { ResponseAPI, LoginResponse, RenewTokenResponse } from "./http";
import type { User } from "./credential";
export type {
@@ -7,7 +7,8 @@ export type {
Navigations,
LabelAndValue,
MailConfig,
- UseHttpProps,
+ ResponseAPI,
+ RenewTokenResponse,
LoginResponse,
User
};
diff --git a/src/routes/(protected)/+layout.ts b/src/routes/(protected)/+layout.ts
index dd687a1..2d1813a 100644
--- a/src/routes/(protected)/+layout.ts
+++ b/src/routes/(protected)/+layout.ts
@@ -1,11 +1,21 @@
import { useAuth } from "$lib/hooks/auth.svelte";
import { redirect } from "@sveltejs/kit";
-import type { LayoutLoad } from "./$types";
+import type { LayoutLoad } from "../$types";
+import http from "$lib/hooks/http.svelte";
+import * as typing from "$typings";
export const load: LayoutLoad = async () => {
const auth = useAuth();
- if (!auth.isValid()) return redirect(307, "/");
+ if (!auth.isValid()) {
+ localStorage.setItem("gwm_invalid_creds", String(1));
+ return redirect(307, "/");
+ }
+ const { data } = await http<typing.User>({
+ params: { action: "get_user_info" }
+ });
+
+ localStorage.setItem("gwm_uinfo", JSON.stringify(data.res));
auth.refresh();
};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 46ef170..8ae2203 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -6,9 +6,10 @@
import { Input } from "$components/ui/input";
import InputPassword from "$components/ui/input/input-password.svelte";
import { useAuth } from "$lib/hooks/auth.svelte";
- import { useHttp } from "$lib/hooks/http.svelte";
+ import http from "$lib/hooks/http.svelte";
import { loginSchema } from "$lib/schemas/login";
- import type { LoginResponse, User } from "$typings";
+ import * as typing from "$typings";
+ import { onMount } from "svelte";
import { superForm, setError, setMessage } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
@@ -21,38 +22,35 @@
validators: zod(loginSchema),
async onUpdate({ form }) {
- const http = useHttp<LoginResponse, User>({
- action: "login",
+ const res = await http<typing.LoginResponse>({
+ params: { action: "login" },
method: "POST",
-
- payload: {
+ data: {
user: form.data.username_or_email,
pass: form.data.password
- },
-
- formatter(data, resp) {
- data = resp?.data.res?.user_info!;
- },
-
- onComplete(resp) {
- auth.save(resp.data.res!);
- },
-
- onError(_, errorMessage) {
- setError(form, "username_or_email", "");
- setError(form, "password", "");
- setMessage(form, errorMessage);
}
});
- await http.execute();
+ if (res.status === 200) {
+ auth.save(res.data.res!);
+ } else {
+ setError(form, "username_or_email", "");
+ setError(form, "password", "");
+ setMessage(form, res.data.res);
+ }
}
});
const isError = () => Boolean($errors.username_or_email && $errors.password);
const isValid = () => Boolean($formData.username_or_email && $formData.password);
+ const isCredentialInvalid = () => Boolean(data.isInvalidCreds && +data.isInvalidCreds);
const { form: formData, errors, message, submitting, constraints, enhance } = form;
+
+ onMount(() => {
+ if (!isCredentialInvalid()) return;
+ localStorage.removeItem("gwm_invalid_creds");
+ });
</script>
<div class="mx-auto flex min-h-screen w-full items-center justify-center px-3 py-2">
@@ -62,11 +60,17 @@
<Card.Title class="text-2xl">GNU/Weeb Mail Login</Card.Title>
<Card.Description>Proceed login to manager your email account</Card.Description>
- {#if isError()}
+ {#if isError() && !isCredentialInvalid()}
<span class="text-sm font-medium text-destructive">
{$message}
</span>
{/if}
+
+ {#if !isError() && isCredentialInvalid()}
+ <span class="text-sm font-medium text-destructive">
+ Invalid credential, please login again.
+ </span>
+ {/if}
</Card.Header>
<Card.Content class="grid gap-4">
diff --git a/src/routes/+page.ts b/src/routes/+page.ts
index c3ac27a..a19ae0e 100644
--- a/src/routes/+page.ts
+++ b/src/routes/+page.ts
@@ -7,8 +7,10 @@ import { redirect } from "@sveltejs/kit";
export const load: PageLoad = async () => {
const auth = useAuth();
+ const isInvalidCreds = localStorage.getItem("gwm_invalid_creds");
+
if (auth.isValid()) return redirect(307, "/home");
const form = await superValidate(zod(loginSchema));
- return { form };
+ return { form, isInvalidCreds };
};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 06/13] chore(schema): rename login schema
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (4 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 05/13] refactor: update HTTP client, typings, and login method Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 07/13] feat(ui): add avatar ui Muhammad Rizki
` (8 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/schemas/{login.ts => login-schema.ts} | 0
src/routes/+page.svelte | 2 +-
src/routes/+page.ts | 2 +-
3 files changed, 2 insertions(+), 2 deletions(-)
rename src/lib/schemas/{login.ts => login-schema.ts} (100%)
diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login-schema.ts
similarity index 100%
rename from src/lib/schemas/login.ts
rename to src/lib/schemas/login-schema.ts
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 8ae2203..9f82bad 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -7,7 +7,7 @@
import InputPassword from "$components/ui/input/input-password.svelte";
import { useAuth } from "$lib/hooks/auth.svelte";
import http from "$lib/hooks/http.svelte";
- import { loginSchema } from "$lib/schemas/login";
+ import { loginSchema } from "$lib/schemas/login-schema.js";
import * as typing from "$typings";
import { onMount } from "svelte";
import { superForm, setError, setMessage } from "sveltekit-superforms";
diff --git a/src/routes/+page.ts b/src/routes/+page.ts
index a19ae0e..8cbe162 100644
--- a/src/routes/+page.ts
+++ b/src/routes/+page.ts
@@ -1,4 +1,4 @@
-import { loginSchema } from "$lib/schemas/login";
+import { loginSchema } from "$lib/schemas/login-schema";
import { superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import type { PageLoad } from "./$types";
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 07/13] feat(ui): add avatar ui
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (5 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 06/13] chore(schema): rename login schema Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 08/13] chore(sidebar-menu): add active menu style Muhammad Rizki
` (7 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Signed-off-by: Muhammad Rizki <[email protected]>
---
package-lock.json | 50 ++++++++-----------
package.json | 2 +-
.../ui/avatar/avatar-fallback.svelte | 16 ++++++
.../components/ui/avatar/avatar-image.svelte | 21 ++++++++
src/lib/components/ui/avatar/avatar.svelte | 18 +++++++
src/lib/components/ui/avatar/index.ts | 13 +++++
6 files changed, 90 insertions(+), 30 deletions(-)
create mode 100644 src/lib/components/ui/avatar/avatar-fallback.svelte
create mode 100644 src/lib/components/ui/avatar/avatar-image.svelte
create mode 100644 src/lib/components/ui/avatar/avatar.svelte
create mode 100644 src/lib/components/ui/avatar/index.ts
diff --git a/package-lock.json b/package-lock.json
index 313f13f..f0201f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
- "bits-ui": "^1.0.0-next.77",
+ "bits-ui": "^1.1.0",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -1898,9 +1898,9 @@
}
},
"node_modules/bits-ui": {
- "version": "1.0.0-next.77",
- "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.77.tgz",
- "integrity": "sha512-IV0AyVEvsRkXv4s/fl4iea5E9W2b9EBf98s9mRMKMc1xHxM9MmtM2r6MZMqftHQ/c+gHTIt3A9EKuTlh7uay8w==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.1.0.tgz",
+ "integrity": "sha512-fnMO3LrNIjWfirm0UJ/kewH5PTi4BobhGpLoZzN2bc/YnsNQ0NcIWkLDJdrrNSTpcfYuwlJpIVGK96tc1FXa5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1908,8 +1908,9 @@
"@floating-ui/dom": "^1.6.7",
"@internationalized/date": "^3.5.6",
"esm-env": "^1.1.2",
- "runed": "^0.22.0",
- "svelte-toolbelt": "^0.7.0"
+ "runed": "^0.23.2",
+ "svelte-toolbelt": "^0.7.1",
+ "tabbable": "^6.2.0"
},
"engines": {
"node": ">=18",
@@ -4245,9 +4246,9 @@
}
},
"node_modules/runed": {
- "version": "0.22.0",
- "resolved": "https://registry.npmjs.org/runed/-/runed-0.22.0.tgz",
- "integrity": "sha512-ZWVXWhOr0P5xdNgtviz6D1ivLUDWKLCbeC5SUEJ3zBkqLReVqWHenFxMNFeFaiC5bfxhFxyxzyzB+98uYFtwdA==",
+ "version": "0.23.4",
+ "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
+ "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte",
@@ -4697,16 +4698,16 @@
}
},
"node_modules/svelte-toolbelt": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.0.tgz",
- "integrity": "sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
+ "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte"
],
"dependencies": {
"clsx": "^2.1.1",
- "runed": "^0.20.0",
+ "runed": "^0.23.2",
"style-to-object": "^1.0.8"
},
"engines": {
@@ -4717,22 +4718,6 @@
"svelte": "^5.0.0"
}
},
- "node_modules/svelte-toolbelt/node_modules/runed": {
- "version": "0.20.0",
- "resolved": "https://registry.npmjs.org/runed/-/runed-0.20.0.tgz",
- "integrity": "sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/huntabyte",
- "https://github.com/sponsors/tglide"
- ],
- "dependencies": {
- "esm-env": "^1.0.0"
- },
- "peerDependencies": {
- "svelte": "^5.7.0"
- }
- },
"node_modules/sveltekit-superforms": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.22.1.tgz",
@@ -4830,6 +4815,13 @@
}
}
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
diff --git a/package.json b/package.json
index 16073a5..623223c 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
- "bits-ui": "^1.0.0-next.77",
+ "bits-ui": "^1.1.0",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte
new file mode 100644
index 0000000..344353c
--- /dev/null
+++ b/src/lib/components/ui/avatar/avatar-fallback.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+ import { Avatar as AvatarPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ class: className,
+ ref = $bindable(null),
+ ...restProps
+ }: AvatarPrimitive.FallbackProps = $props();
+</script>
+
+<AvatarPrimitive.Fallback
+ bind:ref
+ class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
+ {...restProps}
+/>
diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte
new file mode 100644
index 0000000..0878ef3
--- /dev/null
+++ b/src/lib/components/ui/avatar/avatar-image.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+ import { Avatar as AvatarPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ class: className,
+ src,
+ alt,
+ ref = $bindable(null),
+ ...restProps
+ }: AvatarPrimitive.ImageProps = $props();
+</script>
+
+<AvatarPrimitive.Image
+ bind:ref
+ {src}
+ {alt}
+ class={cn("aspect-square h-full w-full", className)}
+ draggable={false}
+ {...restProps}
+/>
diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte
new file mode 100644
index 0000000..019894e
--- /dev/null
+++ b/src/lib/components/ui/avatar/avatar.svelte
@@ -0,0 +1,18 @@
+<script lang="ts">
+ import { Avatar as AvatarPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ class: className,
+ ref = $bindable(null),
+ loadingStatus = $bindable("loading"),
+ ...restProps
+ }: AvatarPrimitive.RootProps = $props();
+</script>
+
+<AvatarPrimitive.Root
+ bind:loadingStatus
+ bind:ref
+ class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
+ {...restProps}
+/>
diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts
new file mode 100644
index 0000000..1efa894
--- /dev/null
+++ b/src/lib/components/ui/avatar/index.ts
@@ -0,0 +1,13 @@
+import Root from "./avatar.svelte";
+import Image from "./avatar-image.svelte";
+import Fallback from "./avatar-fallback.svelte";
+
+export {
+ Root,
+ Image,
+ Fallback,
+ //
+ Root as Avatar,
+ Image as AvatarImage,
+ Fallback as AvatarFallback
+};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 08/13] chore(sidebar-menu): add active menu style
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (6 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 07/13] feat(ui): add avatar ui Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 09/13] chore(ui/avatar): add select-none for avatar fallback Muhammad Rizki
` (6 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added current active menu style to increase user experience.
Signed-off-by: Muhammad Rizki <[email protected]>
---
| 40 +++++++++++++++++--
| 6 +--
2 files changed, 39 insertions(+), 7 deletions(-)
--git a/src/lib/components/customs/app-sidebar.svelte b/src/lib/components/customs/app-sidebar.svelte
index 9291b94..24d0106 100644
--- a/src/lib/components/customs/app-sidebar.svelte
+++ b/src/lib/components/customs/app-sidebar.svelte
@@ -6,13 +6,22 @@
import { LogOut, Mails } from "lucide-svelte";
import type { ComponentProps } from "svelte";
import { goto } from "$app/navigation";
- import Button from "$components/ui/button/button.svelte";
+ import Button from "$components/ui/button/button.svelte";
+ import { cn } from "$utils";
+ import { page } from "$app/state";
+ import { crossfade } from "svelte/transition";
+ import { cubicInOut } from "svelte/easing";
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
const auth = useAuth();
const sidebar = Sidebar.useSidebar();
+ const [send, receive] = crossfade({
+ duration: 250,
+ easing: cubicInOut
+ });
+
const handleNavigationMobile = () => {
if (!sidebar.isMobile) return;
sidebar.toggle();
@@ -49,12 +58,31 @@
<Sidebar.Menu>
{#each navigations as item (item.name)}
<Sidebar.MenuItem>
- <Sidebar.MenuButton>
+ {@const isActive = page.url.pathname.startsWith(item.url)}
+ <Sidebar.MenuButton {isActive} class="relative">
{#snippet child({ props })}
- <a href={item.url} onclick={handleNavigationMobile} {...props}>
+ {@const className = props.class as string}
+ <a
+ href={item.url}
+ onclick={handleNavigationMobile}
+ {...props}
+ class={cn("relative z-10", className)}
+ >
<item.icon />
<span>{item.name}</span>
</a>
+ {#if isActive}
+ <!-- svelte-ignore element_invalid_self_closing_tag -->
+ <div
+ class={cn(
+ "absolute inset-0 rounded-md bg-sidebar-accent",
+ isActive &&
+ "overflow-hidden before:absolute before:left-0 before:h-full before:w-0.5 before:bg-foreground"
+ )}
+ in:send={{ key: "active-sidebar-tab" }}
+ out:receive={{ key: "active-sidebar-tab" }}
+ />
+ {/if}
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
@@ -65,7 +93,11 @@
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Menu>
- <Button variant="destructive" onclick={handleLogout} class="flex justify-start items-center w-full">
+ <Button
+ variant="destructive"
+ onclick={handleLogout}
+ class="flex w-full items-center justify-start"
+ >
<LogOut />
<span>Logout</span>
</Button>
--git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
index e14dc5f..5e9e002 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -2,12 +2,12 @@
import { tv, type VariantProps } from "tailwind-variants";
export const sidebarMenuButtonVariants = tv({
- base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ base: "peer/menu-button ring-sidebar-ring hover:underline hover:text-sidebar-accent-foreground active:underline active:text-sidebar-accent-foreground data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:underline data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
variants: {
variant: {
- default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ default: "hover:underline hover:text-sidebar-accent-foreground",
outline:
- "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
+ "bg-background hover:underline hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
},
size: {
default: "h-8 text-sm",
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 09/13] chore(ui/avatar): add select-none for avatar fallback
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (7 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 08/13] chore(sidebar-menu): add active menu style Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 10/13] chore(deps): upgrade bits-ui version Muhammad Rizki
` (5 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added select-none style to prevent user selecting a fallback text.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/components/ui/avatar/avatar-fallback.svelte | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte
index 344353c..b509918 100644
--- a/src/lib/components/ui/avatar/avatar-fallback.svelte
+++ b/src/lib/components/ui/avatar/avatar-fallback.svelte
@@ -11,6 +11,9 @@
<AvatarPrimitive.Fallback
bind:ref
- class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
+ class={cn(
+ "flex h-full w-full select-none items-center justify-center rounded-full bg-muted",
+ className
+ )}
{...restProps}
/>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 10/13] chore(deps): upgrade bits-ui version
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (8 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 09/13] chore(ui/avatar): add select-none for avatar fallback Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 11/13] feat(ui): add radio-group ui Muhammad Rizki
` (4 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Signed-off-by: Muhammad Rizki <[email protected]>
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f0201f2..b9d8ba9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
- "bits-ui": "^1.1.0",
+ "bits-ui": "^1.3.2",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -1898,9 +1898,9 @@
}
},
"node_modules/bits-ui": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.1.0.tgz",
- "integrity": "sha512-fnMO3LrNIjWfirm0UJ/kewH5PTi4BobhGpLoZzN2bc/YnsNQ0NcIWkLDJdrrNSTpcfYuwlJpIVGK96tc1FXa5Q==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.2.tgz",
+ "integrity": "sha512-27fg/O71yqYmDaDf/opInylQYJjBNlFw3Ari0mXKLA6UrQpRvY62EsAid8s+ci/wYrGZMvHpzNJ69t15ycBHTw==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 623223c..c323150 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
- "bits-ui": "^1.1.0",
+ "bits-ui": "^1.3.2",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 11/13] feat(ui): add radio-group ui
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (9 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 10/13] chore(deps): upgrade bits-ui version Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 12/13] feat(sidebar-menu): add Roundcube link Muhammad Rizki
` (3 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/components/ui/radio-group/index.ts | 10 +++++++
.../ui/radio-group/radio-group-item.svelte | 30 +++++++++++++++++++
.../ui/radio-group/radio-group.svelte | 13 ++++++++
3 files changed, 53 insertions(+)
create mode 100644 src/lib/components/ui/radio-group/index.ts
create mode 100644 src/lib/components/ui/radio-group/radio-group-item.svelte
create mode 100644 src/lib/components/ui/radio-group/radio-group.svelte
diff --git a/src/lib/components/ui/radio-group/index.ts b/src/lib/components/ui/radio-group/index.ts
new file mode 100644
index 0000000..86c6131
--- /dev/null
+++ b/src/lib/components/ui/radio-group/index.ts
@@ -0,0 +1,10 @@
+import Root from "./radio-group.svelte";
+import Item from "./radio-group-item.svelte";
+
+export {
+ Root,
+ Item,
+ //
+ Root as RadioGroup,
+ Item as RadioGroupItem
+};
diff --git a/src/lib/components/ui/radio-group/radio-group-item.svelte b/src/lib/components/ui/radio-group/radio-group-item.svelte
new file mode 100644
index 0000000..646ab35
--- /dev/null
+++ b/src/lib/components/ui/radio-group/radio-group-item.svelte
@@ -0,0 +1,30 @@
+<script lang="ts">
+ import { RadioGroup as RadioGroupPrimitive, type WithoutChildrenOrChild } from "bits-ui";
+ import Circle from "lucide-svelte/icons/circle";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ ...restProps
+ }: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> & {
+ value: string;
+ } = $props();
+</script>
+
+<RadioGroupPrimitive.Item
+ bind:ref
+ class={cn(
+ "aspect-square size-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
+ className
+ )}
+ {...restProps}
+>
+ {#snippet children({ checked })}
+ <div class="flex items-center justify-center">
+ {#if checked}
+ <Circle class="size-3.5 fill-primary" />
+ {/if}
+ </div>
+ {/snippet}
+</RadioGroupPrimitive.Item>
diff --git a/src/lib/components/ui/radio-group/radio-group.svelte b/src/lib/components/ui/radio-group/radio-group.svelte
new file mode 100644
index 0000000..e2b0147
--- /dev/null
+++ b/src/lib/components/ui/radio-group/radio-group.svelte
@@ -0,0 +1,13 @@
+<script lang="ts">
+ import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ value = $bindable(),
+ ...restProps
+ }: RadioGroupPrimitive.RootProps = $props();
+</script>
+
+<RadioGroupPrimitive.Root bind:value class={cn("grid gap-2", className)} {...restProps} bind:ref />
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 12/13] feat(sidebar-menu): add Roundcube link
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (10 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 11/13] feat(ui): add radio-group ui Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 13/13] feat: add settings pages Muhammad Rizki
` (2 subsequent siblings)
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added Roundcube link on sidebar menu, this commit include IconRoundcube
icon component too.
Signed-off-by: Muhammad Rizki <[email protected]>
---
| 22 +++++++++++++-
.../components/icons/icon-roundcube.svelte | 29 +++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 src/lib/components/icons/icon-roundcube.svelte
--git a/src/lib/components/customs/app-sidebar.svelte b/src/lib/components/customs/app-sidebar.svelte
index 24d0106..2fba975 100644
--- a/src/lib/components/customs/app-sidebar.svelte
+++ b/src/lib/components/customs/app-sidebar.svelte
@@ -3,7 +3,7 @@
import Separator from "$components/ui/separator/separator.svelte";
import * as Sidebar from "$lib/components/ui/sidebar";
import { useAuth } from "$lib/hooks/auth.svelte";
- import { LogOut, Mails } from "lucide-svelte";
+ import { LogOut, Mails, SquareArrowOutUpRight } from "lucide-svelte";
import type { ComponentProps } from "svelte";
import { goto } from "$app/navigation";
import Button from "$components/ui/button/button.svelte";
@@ -11,6 +11,7 @@
import { page } from "$app/state";
import { crossfade } from "svelte/transition";
import { cubicInOut } from "svelte/easing";
+ import IconRoundcube from "$components/icons/icon-roundcube.svelte";
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
@@ -87,6 +88,25 @@
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
+ <Sidebar.MenuItem>
+ <Sidebar.MenuButton>
+ {#snippet child({ props })}
+ {@const className = props.class as string}
+ <a
+ href="https://mail.gnuweeb.org/roundcube/"
+ {...props}
+ class={cn("group/roundcube", className)}
+ target="_blank"
+ >
+ <IconRoundcube />
+ <span>Roundcube</span>
+ <SquareArrowOutUpRight
+ class="!size-3 transition-transform group-hover/roundcube:!scale-125"
+ />
+ </a>
+ {/snippet}
+ </Sidebar.MenuButton>
+ </Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
diff --git a/src/lib/components/icons/icon-roundcube.svelte b/src/lib/components/icons/icon-roundcube.svelte
new file mode 100644
index 0000000..8796ad2
--- /dev/null
+++ b/src/lib/components/icons/icon-roundcube.svelte
@@ -0,0 +1,29 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.14 141.8 573.65 573.65">
+ <style>
+ .st0,
+ .st3 {
+ fill-rule: evenodd;
+ clip-rule: evenodd;
+ fill: #404f54;
+ }
+ .st3 {
+ fill: #37beff;
+ }
+ </style>
+ <path class="st3" d="M582.79 549.77L295.96 384.1V207.27l286.83 165.68z" />
+ <path class="st0" d="M9.14 549.77L295.96 384.1V207.27L9.14 372.95z" />
+ <path
+ d="M295.96 141.8c109.56 0 198.41 88.85 198.41 198.41s-88.85 198.41-198.41 198.41S97.55 449.77 97.55 340.21 186.4 141.8 295.96 141.8"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ fill="#ccc"
+ />
+ <path
+ d="M295.96 141.8c109.6 0 198.48 88.85 198.48 198.41s-88.88 198.41-198.48 198.41c-62.91-42.34-88.94-127.64-88.94-198.3s26.03-156.1 88.94-198.52"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ fill="#e5e5e5"
+ />
+ <path class="st3" d="M582.79 372.95L295.96 538.62v176.83l286.83-165.68z" />
+ <path class="st0" d="M9.14 372.95l286.82 165.67v176.83L9.14 549.77z" />
+</svg>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [PATCH v1 13/13] feat: add settings pages
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (11 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 12/13] feat(sidebar-menu): add Roundcube link Muhammad Rizki
@ 2025-02-22 22:54 ` Muhammad Rizki
2025-02-23 8:27 ` [PATCH v1 00/13] Add Profile & Account Management Alviro Iskandar Setiawan
2025-02-23 8:43 ` Ammar Faizi
14 siblings, 0 replies; 19+ messages in thread
From: Muhammad Rizki @ 2025-02-22 22:54 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
This commit adds profile management page and account management page,
including its schemas and local UI components.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/schemas/account-schema.ts | 12 ++
src/lib/schemas/profile-schema.ts | 8 +
| 13 ++
.../settings/(components)/settings-nav.svelte | 45 +++++
.../(protected)/settings/+layout.svelte | 28 +++
src/routes/(protected)/settings/+page.ts | 11 ++
.../(protected)/settings/account/+page.svelte | 107 +++++++++++
.../(protected)/settings/account/+page.ts | 9 +
.../(protected)/settings/profile/+page.svelte | 170 ++++++++++++++++++
.../(protected)/settings/profile/+page.ts | 20 +++
10 files changed, 423 insertions(+)
create mode 100644 src/lib/schemas/account-schema.ts
create mode 100644 src/lib/schemas/profile-schema.ts
create mode 100644 src/routes/(protected)/settings/(components)/settings-header.svelte
create mode 100644 src/routes/(protected)/settings/(components)/settings-nav.svelte
create mode 100644 src/routes/(protected)/settings/+layout.svelte
create mode 100644 src/routes/(protected)/settings/+page.ts
create mode 100644 src/routes/(protected)/settings/account/+page.svelte
create mode 100644 src/routes/(protected)/settings/account/+page.ts
create mode 100644 src/routes/(protected)/settings/profile/+page.svelte
create mode 100644 src/routes/(protected)/settings/profile/+page.ts
diff --git a/src/lib/schemas/account-schema.ts b/src/lib/schemas/account-schema.ts
new file mode 100644
index 0000000..d633405
--- /dev/null
+++ b/src/lib/schemas/account-schema.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+
+export const accountSchema = z
+ .object({
+ cur_pass: z.string(),
+ new_pass: z.string(),
+ retype_new_pass: z.string()
+ })
+ .refine(({ new_pass, retype_new_pass }) => new_pass === retype_new_pass, {
+ message: "The new password does not match with the retyped one.",
+ path: ["retype_new_pass"]
+ });
diff --git a/src/lib/schemas/profile-schema.ts b/src/lib/schemas/profile-schema.ts
new file mode 100644
index 0000000..fa67c17
--- /dev/null
+++ b/src/lib/schemas/profile-schema.ts
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const profileSchema = z.object({
+ avatar: z.instanceof(File).optional(),
+ username: z.string().optional(),
+ full_name: z.string().optional(),
+ gender: z.string().optional()
+});
--git a/src/routes/(protected)/settings/(components)/settings-header.svelte b/src/routes/(protected)/settings/(components)/settings-header.svelte
new file mode 100644
index 0000000..cc13950
--- /dev/null
+++ b/src/routes/(protected)/settings/(components)/settings-header.svelte
@@ -0,0 +1,13 @@
+<script lang="ts">
+ import { page } from "$app/state";
+ import { settingsNav } from "$constants/navigations";
+ import Separator from "$components/ui/separator/separator.svelte";
+
+ const activeNav = $derived(settingsNav.find((e) => page.url.pathname === e.url));
+</script>
+
+<div>
+ <h3 class="font-medium">{activeNav?.name}</h3>
+ <p class="text-sm text-muted-foreground">{activeNav?.description}</p>
+</div>
+<Separator />
diff --git a/src/routes/(protected)/settings/(components)/settings-nav.svelte b/src/routes/(protected)/settings/(components)/settings-nav.svelte
new file mode 100644
index 0000000..1ff5968
--- /dev/null
+++ b/src/routes/(protected)/settings/(components)/settings-nav.svelte
@@ -0,0 +1,45 @@
+<script lang="ts">
+ import { cubicInOut } from "svelte/easing";
+ import { crossfade } from "svelte/transition";
+ import { cn } from "$utils";
+ import { page } from "$app/state";
+ import type { HTMLAttributes } from "svelte/elements";
+ import Button from "$components/ui/button/button.svelte";
+ import * as typing from "$typings";
+
+ interface Props extends HTMLAttributes<HTMLDivElement> {
+ items: typing.Navigations[];
+ }
+
+ let { items, class: className }: Props = $props();
+
+ const [send, receive] = crossfade({
+ duration: 250,
+ easing: cubicInOut
+ });
+</script>
+
+<nav class={cn("flex space-x-2 xl:flex-col xl:space-x-0 xl:space-y-1", className)}>
+ {#each items as item}
+ {@const isActive = page.url.pathname === item.url}
+ <Button
+ href={item.url}
+ variant="ghost"
+ class={cn(!isActive && "hover:underline", "relative justify-start hover:bg-transparent")}
+ disabled={item.disabled}
+ data-sveltekit-noscroll
+ >
+ {#if isActive}
+ <!-- svelte-ignore element_invalid_self_closing_tag -->
+ <div
+ class="absolute inset-0 rounded-md bg-muted"
+ in:send={{ key: "active-sidebar-tab" }}
+ out:receive={{ key: "active-sidebar-tab" }}
+ />
+ {/if}
+ <div class="relative">
+ {item.name}
+ </div>
+ </Button>
+ {/each}
+</nav>
diff --git a/src/routes/(protected)/settings/+layout.svelte b/src/routes/(protected)/settings/+layout.svelte
new file mode 100644
index 0000000..3ef02e4
--- /dev/null
+++ b/src/routes/(protected)/settings/+layout.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+ import { Separator } from "$lib/components/ui/separator";
+
+ import { settingsNav } from "$constants/navigations";
+ import SettingsNavigation from "./(components)/settings-nav.svelte";
+ import SettingsHeader from "./(components)/settings-header.svelte";
+
+ let { children } = $props();
+</script>
+
+<div class="space-y-6 pb-16 prose-h2:text-2xl prose-h3:text-lg">
+ <div class="space-y-0.5">
+ <h2 class="font-bold tracking-tight">Settings</h2>
+ <p class="text-muted-foreground">Manage your account settings and set e-mail preferences.</p>
+ </div>
+ <Separator class="my-6" />
+ <div class="flex flex-col space-y-8 xl:flex-row xl:space-x-8 xl:space-y-0">
+ <aside class="xl:w-[12%]">
+ <SettingsNavigation items={settingsNav} />
+ </aside>
+ <div class="flex-1">
+ <div class="space-y-6">
+ <SettingsHeader />
+ {@render children()}
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/routes/(protected)/settings/+page.ts b/src/routes/(protected)/settings/+page.ts
new file mode 100644
index 0000000..3c846ad
--- /dev/null
+++ b/src/routes/(protected)/settings/+page.ts
@@ -0,0 +1,11 @@
+import { redirect } from "@sveltejs/kit";
+import type { PageLoad } from "./$types";
+import { settingsNav } from "$constants/navigations";
+
+export const load: PageLoad = async () => {
+ // get first page that are not disabled.
+ const firstPage = settingsNav.find((e) => !e.disabled);
+
+ // if it don't exist, redirect to index page.
+ return redirect(307, firstPage?.url ?? "/");
+};
diff --git a/src/routes/(protected)/settings/account/+page.svelte b/src/routes/(protected)/settings/account/+page.svelte
new file mode 100644
index 0000000..00590bf
--- /dev/null
+++ b/src/routes/(protected)/settings/account/+page.svelte
@@ -0,0 +1,107 @@
+<script lang="ts">
+ import { accountSchema } from "$lib/schemas/account-schema";
+ import { setError, superForm } from "sveltekit-superforms";
+ import { zodClient } from "sveltekit-superforms/adapters";
+ import { toast } from "svelte-sonner";
+ import * as Form from "$components/ui/form";
+ import InputPassword from "$components/ui/input/input-password.svelte";
+ import Button from "$components/ui/button/button.svelte";
+ import http from "$lib/hooks/http.svelte";
+
+ let { data } = $props();
+
+ const form = superForm(data.form, {
+ SPA: true,
+ validators: zodClient(accountSchema),
+ validationMethod: "oninput",
+
+ async onUpdate({ form }) {
+ const res = await http<string>({
+ params: { action: "change_password" },
+ method: "POST",
+ data: form.data
+ });
+
+ const message = res.data.res as string;
+
+ if (res.status >= 400) {
+ if (message.includes("current password is wrong")) {
+ setError(form, "cur_pass", message);
+ } else {
+ setError(form, "new_pass", message);
+ setError(form, "retype_new_pass", message);
+ }
+ } else {
+ reset();
+ toast.info(message);
+ }
+ }
+ });
+
+ const { form: formData, errors, submitting, constraints, enhance, reset } = form;
+
+ const isSubmittable = $derived(
+ Boolean($formData.cur_pass && $formData.new_pass && $formData.retype_new_pass)
+ );
+
+ const isError = $derived(
+ Boolean($errors.cur_pass || $errors.new_pass || $errors.retype_new_pass)
+ );
+</script>
+
+<form use:enhance class="space-y-5">
+ <Form.Field {form} name="cur_pass">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>Current Password</Form.Label>
+ <InputPassword
+ {...props}
+ aria-invalid={$errors.cur_pass ? "true" : undefined}
+ bind:value={$formData.cur_pass}
+ placeholder="Enter current password"
+ disabled={$submitting}
+ {...$constraints.cur_pass}
+ />
+ <Form.FieldErrors />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="new_pass">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>New Password</Form.Label>
+ <InputPassword
+ {...props}
+ aria-invalid={$errors.new_pass ? "true" : undefined}
+ bind:value={$formData.new_pass}
+ placeholder="Enter new password"
+ disabled={$submitting}
+ {...$constraints.new_pass}
+ />
+ <Form.FieldErrors />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="retype_new_pass">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>Re-type New Password</Form.Label>
+ <InputPassword
+ {...props}
+ aria-invalid={$errors.retype_new_pass ? "true" : undefined}
+ bind:value={$formData.retype_new_pass}
+ placeholder="Re-type new password"
+ disabled={$submitting}
+ {...$constraints.retype_new_pass}
+ />
+ <Form.FieldErrors />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Button type="submit" class="px-8" disabled={$submitting || !isSubmittable || isError}>
+ Submit
+ </Button>
+</form>
diff --git a/src/routes/(protected)/settings/account/+page.ts b/src/routes/(protected)/settings/account/+page.ts
new file mode 100644
index 0000000..8089d3e
--- /dev/null
+++ b/src/routes/(protected)/settings/account/+page.ts
@@ -0,0 +1,9 @@
+import type { PageLoad } from "./$types";
+import { zod } from "sveltekit-superforms/adapters";
+import { superValidate } from "sveltekit-superforms";
+import { accountSchema } from "$lib/schemas/account-schema";
+
+export const load: PageLoad = async () => {
+ const form = await superValidate(zod(accountSchema));
+ return { form };
+};
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
new file mode 100644
index 0000000..442e271
--- /dev/null
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -0,0 +1,170 @@
+<script lang="ts">
+ import { useAuth } from "$lib/hooks/auth.svelte";
+ import { superForm } from "sveltekit-superforms";
+ import { zodClient } from "sveltekit-superforms/adapters";
+ import { profileSchema } from "$lib/schemas/profile-schema";
+ import { Pencil } from "lucide-svelte";
+ import * as Avatar from "$lib/components/ui/avatar";
+ import * as Form from "$lib/components/ui/form";
+ import * as RadioGroup from "$lib/components/ui/radio-group";
+ import Input from "$components/ui/input/input.svelte";
+ import Label from "$components/ui/label/label.svelte";
+
+ let { data } = $props();
+
+ const auth = useAuth();
+
+ let avatarImage = $state<string>();
+
+ const form = superForm(data.form, {
+ SPA: true,
+ validators: zodClient(profileSchema),
+ });
+
+ const getShortName = () => {
+ const fullName = auth.user?.full_name ?? "";
+ const match = fullName.match(/\b(\w)/g) ?? [];
+ return match.slice(0, 2).join("");
+ };
+
+ const handleAvatar = (e: any) => {
+ const file = e.srcElement.files[0];
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+
+ reader.onload = function () {
+ avatarImage = reader.result as string;
+ };
+ };
+
+ const { form: formData, errors, submitting, constraints, enhance } = form;
+</script>
+
+<form
+ use:enhance
+ class="flex w-full max-w-5xl flex-col gap-y-8 lg:flex-row lg:justify-between lg:gap-x-8 lg:gap-y-0"
+>
+ <div class="flex w-full justify-center lg:hidden">
+ <Form.Field {form} name="avatar">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="avatar" class="relative cursor-pointer">
+ <Avatar.Root class="size-40">
+ <Avatar.Image src={avatarImage} alt="@{auth.user?.username}" />
+ <Avatar.Fallback class="text-xl">{getShortName()}</Avatar.Fallback>
+ </Avatar.Root>
+ <div
+ class="absolute bottom-3 left-0 flex items-center gap-x-1 rounded-lg bg-foreground px-2 py-1 text-primary-foreground"
+ >
+ <Pencil class="size-4" />
+ <span class="text-xs font-medium">Edit</span>
+ </div>
+ </Form.Label>
+ <Input
+ type="file"
+ accept="image/png,image/jpeg"
+ {...props}
+ aria-invalid={$errors.avatar ? "true" : undefined}
+ bind:value={$formData.avatar}
+ disabled={$submitting}
+ {...$constraints.avatar}
+ class="hidden"
+ onchange={handleAvatar}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+
+ <div class="flex w-full max-w-3xl flex-col space-y-5">
+ <Form.Field {form} name="username">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>Username</Form.Label>
+ <Input
+ {...props}
+ aria-invalid={$errors.username ? "true" : undefined}
+ bind:value={$formData.username}
+ placeholder="@username"
+ {...$constraints.username}
+ disabled
+ />
+ <Form.Description>This is your GNU/Weeb email username.</Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="full_name">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>Full Name</Form.Label>
+ <Input
+ {...props}
+ aria-invalid={$errors.full_name ? "true" : undefined}
+ bind:value={$formData.full_name}
+ placeholder="Your full name"
+ {...$constraints.full_name}
+ disabled
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="gender">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label>Gender</Form.Label>
+ <RadioGroup.Root
+ {...props}
+ aria-invalid={$errors.gender ? "true" : undefined}
+ bind:value={$formData.gender}
+ placeholder="Your full name"
+ {...$constraints.gender}
+ disabled
+ >
+ <div class="flex items-center space-x-2">
+ <RadioGroup.Item value="m" id="m" />
+ <Label for="m">Male</Label>
+ </div>
+ <div class="flex items-center space-x-2">
+ <RadioGroup.Item value="f" id="f" />
+ <Label for="f">Female</Label>
+ </div>
+ </RadioGroup.Root>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+
+ <div class="hidden lg:block">
+ <Form.Field {form} name="avatar">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="avatar" class="relative cursor-pointer">
+ <Avatar.Root class="lg:size-40 xl:size-52">
+ <Avatar.Image src={avatarImage} alt="@{auth.user?.username}" />
+ <Avatar.Fallback class="lg:text-xl xl:text-3xl">{getShortName()}</Avatar.Fallback>
+ </Avatar.Root>
+ <div
+ class="absolute bottom-3 left-0 flex items-center gap-x-1 rounded-lg bg-foreground px-2 py-1 text-primary-foreground xl:left-2.5"
+ >
+ <Pencil class="size-4" />
+ <span class="text-xs font-medium">Edit</span>
+ </div>
+ </Form.Label>
+ <Input
+ type="file"
+ accept="image/png,image/jpeg"
+ {...props}
+ aria-invalid={$errors.avatar ? "true" : undefined}
+ bind:value={$formData.avatar}
+ disabled={$submitting}
+ {...$constraints.avatar}
+ class="hidden"
+ onchange={handleAvatar}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+</form>
diff --git a/src/routes/(protected)/settings/profile/+page.ts b/src/routes/(protected)/settings/profile/+page.ts
new file mode 100644
index 0000000..9e036c8
--- /dev/null
+++ b/src/routes/(protected)/settings/profile/+page.ts
@@ -0,0 +1,20 @@
+import type { PageLoad } from "./$types";
+import { zod } from "sveltekit-superforms/adapters";
+import { superValidate } from "sveltekit-superforms";
+import { profileSchema } from "$lib/schemas/profile-schema";
+import { useAuth } from "$lib/hooks/auth.svelte";
+
+export const load: PageLoad = async () => {
+ const auth = useAuth();
+ auth.refresh();
+
+ const data = {
+ username: auth.user?.username,
+ full_name: auth.user?.full_name,
+ gender: auth.user?.gender
+ };
+
+ const form = await superValidate(data, zod(profileSchema));
+
+ return { form };
+};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 19+ messages in thread
* Re: [PATCH v1 00/13] Add Profile & Account Management
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (12 preceding siblings ...)
2025-02-22 22:54 ` [PATCH v1 13/13] feat: add settings pages Muhammad Rizki
@ 2025-02-23 8:27 ` Alviro Iskandar Setiawan
2025-02-23 8:43 ` Ammar Faizi
14 siblings, 0 replies; 19+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-02-23 8:27 UTC (permalink / raw)
To: Muhammad Rizki; +Cc: Ammar Faizi, GNU/Weeb Mailing List
On Sun, Feb 23, 2025 at 5:54 AM Muhammad Rizki wrote:
> This series adds settings page for user account management.
> Improvements and optimizations were applied in this series, it
> should be no issue.
Acked-by: Alviro Iskandar Setiawan <[email protected]>
-- Viro
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v1 00/13] Add Profile & Account Management
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
` (13 preceding siblings ...)
2025-02-23 8:27 ` [PATCH v1 00/13] Add Profile & Account Management Alviro Iskandar Setiawan
@ 2025-02-23 8:43 ` Ammar Faizi
2025-02-23 8:52 ` Alviro Iskandar Setiawan
14 siblings, 1 reply; 19+ messages in thread
From: Ammar Faizi @ 2025-02-23 8:43 UTC (permalink / raw)
To: Muhammad Rizki
Cc: Ammar Faizi, GNU/Weeb Mailing List, Alviro Iskandar Setiawan
On Sun, 23 Feb 2025 05:54:06 +0700, Muhammad Rizki wrote:
> This series adds settings page for user account management.
> Improvements and optimizations were applied in this series, it
> should be no issue.
>
> Note:
> Profile page currently is not editable, API is not available, yet.
>
> [...]
Applied, thanks!
It's now live at https://mail.gnuweeb.org/
[01/13] fix(toaster): add Toaster component
commit: a327847aa58cbdb6adaf8d096563c3bbc3cd7a13
[02/13] chore(login): remove unnecessary default data
commit: 08115eb3a6c80748975f2b80ec0f818cf028564c
[03/13] feat(constants): add settingsNav data
commit: 802078c8967e384d39432d125101f9fc8e4e57a5
[04/13] feat(typings/common): add disabled property for Navigations
commit: dc159e3656d278656248e21226610146eef09ea3
[05/13] refactor: update HTTP client, typings, and login method
commit: 577b9d834154ba4e34b510e9f9ed9fb768e53875
[06/13] chore(schema): rename login schema
commit: 928279a4de6bf798dfa0b805ddd1a7c51904f412
[07/13] feat(ui): add avatar ui
commit: f755676aabb3f4a9ac3f0cc6438846d08b4ff3eb
[08/13] chore(sidebar-menu): add active menu style
commit: 83f2d0b15c8b9498ee1d913bfe04ce782815225b
[09/13] chore(ui/avatar): add select-none for avatar fallback
commit: 96f4cae39fb59013c191de9217c38f8f67710530
[10/13] chore(deps): upgrade bits-ui version
commit: 65183b3410e839061ea3f00876b56a3dbb10c4b1
[11/13] feat(ui): add radio-group ui
commit: 6bf6056614aca068ba9f19ade020664addeb34d2
[12/13] feat(sidebar-menu): add Roundcube link
commit: 50e96c03c30abc30312e053041876d6ed8faf8cd
[13/13] feat: add settings pages
commit: 3a19417eed1530408c6a284c8747937657b27469
Best regards,
--
Ammar Faizi
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v1 00/13] Add Profile & Account Management
2025-02-23 8:43 ` Ammar Faizi
@ 2025-02-23 8:52 ` Alviro Iskandar Setiawan
2025-02-23 8:55 ` Ammar Faizi
0 siblings, 1 reply; 19+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-02-23 8:52 UTC (permalink / raw)
To: Ammar Faizi; +Cc: Muhammad Rizki, GNU/Weeb Mailing List
On Sun, Feb 23, 2025 at 3:43 PM Ammar Faizi wrote:
> On Sun, 23 Feb 2025 05:54:06 +0700, Muhammad Rizki wrote:
> > This series adds settings page for user account management.
> > Improvements and optimizations were applied in this series, it
> > should be no issue.
> >
> > Note:
> > Profile page currently is not editable, API is not available, yet.
> >
> > [...]
>
> Applied, thanks!
>
> It's now live at https://mail.gnuweeb.org/
Looks good, that's how a modern web should look like :>
BTW, the features in the settings tab don't work yet?
-- Viro
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v1 00/13] Add Profile & Account Management
2025-02-23 8:52 ` Alviro Iskandar Setiawan
@ 2025-02-23 8:55 ` Ammar Faizi
2025-02-23 8:57 ` Alviro Iskandar Setiawan
0 siblings, 1 reply; 19+ messages in thread
From: Ammar Faizi @ 2025-02-23 8:55 UTC (permalink / raw)
To: Alviro Iskandar Setiawan; +Cc: Muhammad Rizki, GNU/Weeb Mailing List
On Sun, Feb 23, 2025 at 03:52:03PM +0700, Alviro Iskandar Setiawan wrote:
> On Sun, Feb 23, 2025 at 3:43 PM Ammar Faizi wrote:
> > On Sun, 23 Feb 2025 05:54:06 +0700, Muhammad Rizki wrote:
> > > This series adds settings page for user account management.
> > > Improvements and optimizations were applied in this series, it
> > > should be no issue.
> > >
> > > Note:
> > > Profile page currently is not editable, API is not available, yet.
> > >
> > > [...]
> >
> > Applied, thanks!
> >
> > It's now live at https://mail.gnuweeb.org/
>
> Looks good, that's how a modern web should look like :>
>
> BTW, the features in the settings tab don't work yet?
For now, only "change password" feature that works. That is the only
feature needed for emergency, so it was prioritized.
I will publish more API functions to complete the settings page.
--
Ammar Faizi
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v1 00/13] Add Profile & Account Management
2025-02-23 8:55 ` Ammar Faizi
@ 2025-02-23 8:57 ` Alviro Iskandar Setiawan
0 siblings, 0 replies; 19+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-02-23 8:57 UTC (permalink / raw)
To: Ammar Faizi; +Cc: Muhammad Rizki, GNU/Weeb Mailing List
On Sun, Feb 23, 2025 at 3:55 PM Ammar Faizi wrote:
> On Sun, Feb 23, 2025 at 03:52:03PM +0700, Alviro Iskandar Setiawan wrote:
> > On Sun, Feb 23, 2025 at 3:43 PM Ammar Faizi wrote:
> > > On Sun, 23 Feb 2025 05:54:06 +0700, Muhammad Rizki wrote:
> > > > This series adds settings page for user account management.
> > > > Improvements and optimizations were applied in this series, it
> > > > should be no issue.
> > > >
> > > > Note:
> > > > Profile page currently is not editable, API is not available, yet.
> > > >
> > > > [...]
> > >
> > > Applied, thanks!
> > >
> > > It's now live at https://mail.gnuweeb.org/
> >
> > Looks good, that's how a modern web should look like :>
> >
> > BTW, the features in the settings tab don't work yet?
>
> For now, only "change password" feature that works. That is the only
> feature needed for emergency, so it was prioritized.
>
> I will publish more API functions to complete the settings page.
ic ic, tq. Waiting for it...
-- Viro
^ permalink raw reply [flat|nested] 19+ messages in thread