feat: enhance password validation across forms and improve button styling. Update password requirements to include uppercase, lowercase, and numeric characters in sign-up and account settings. Adjust button styles in sign-in and sign-up forms for a consistent look.
Browse files- apps/web/src/components/TourGuide.tsx +1 -1
- apps/web/src/components/settings/AccountSettings.tsx +4 -3
- apps/web/src/components/sign-in-form.tsx +1 -1
- apps/web/src/components/sign-up-form.tsx +2 -2
- apps/web/src/routes/__root.tsx +0 -2
- apps/web/src/routes/forgot-password.tsx +2 -2
- apps/web/src/routes/index.tsx +3 -0
- apps/web/src/routes/me.tsx +13 -0
- packages/api/src/routers/verification.ts +1 -1
apps/web/src/components/TourGuide.tsx
CHANGED
|
@@ -32,7 +32,7 @@ const globalSteps: Step[] = [
|
|
| 32 |
{ target: "[data-tour='nav-bank']", title: "Bank Soal — Buat Paket", content: "Atur soal-soal kamu jadi paket latihan dari bank soal.", spotlightPadding: 4 },
|
| 33 |
{ target: "[data-tour='nav-packages']", title: "Paket Soal — Mulai Latihan", content: "Temukan paket soal dari komunitas atau buatan sendiri.", spotlightPadding: 4 },
|
| 34 |
{ target: "[data-tour='nav-analytics']", title: "Analytics — Evaluasi", content: "Lihat analitik mendalam: skor, tren, dan rekomendasi belajar.", spotlightPadding: 4 },
|
| 35 |
-
{ target: "body" as any, placement: "center", title: "Siap Belajar?", content: "Setiap halaman punya panduan sendiri. Cari tombol
|
| 36 |
];
|
| 37 |
|
| 38 |
export function TourGuide() {
|
|
|
|
| 32 |
{ target: "[data-tour='nav-bank']", title: "Bank Soal — Buat Paket", content: "Atur soal-soal kamu jadi paket latihan dari bank soal.", spotlightPadding: 4 },
|
| 33 |
{ target: "[data-tour='nav-packages']", title: "Paket Soal — Mulai Latihan", content: "Temukan paket soal dari komunitas atau buatan sendiri.", spotlightPadding: 4 },
|
| 34 |
{ target: "[data-tour='nav-analytics']", title: "Analytics — Evaluasi", content: "Lihat analitik mendalam: skor, tren, dan rekomendasi belajar.", spotlightPadding: 4 },
|
| 35 |
+
{ target: "body" as any, placement: "center", title: "Siap Belajar?", content: "Setiap halaman punya panduan sendiri. Cari tombol ? di pojok kanan bawah halaman!", hideOverlay: true },
|
| 36 |
];
|
| 37 |
|
| 38 |
export function TourGuide() {
|
apps/web/src/components/settings/AccountSettings.tsx
CHANGED
|
@@ -20,8 +20,8 @@ export function AccountSettings() {
|
|
| 20 |
|
| 21 |
const handleChangePassword = async (e: React.FormEvent) => {
|
| 22 |
e.preventDefault();
|
| 23 |
-
if (
|
| 24 |
-
toast.error("Password minimal 8 karakter");
|
| 25 |
return;
|
| 26 |
}
|
| 27 |
if (newPassword !== confirmPassword) {
|
|
@@ -123,6 +123,7 @@ export function AccountSettings() {
|
|
| 123 |
</div>
|
| 124 |
<Button
|
| 125 |
type="submit"
|
|
|
|
| 126 |
disabled={isChangingPassword || !currentPassword || !newPassword || !confirmPassword}
|
| 127 |
>
|
| 128 |
{isChangingPassword ? "Menyimpan..." : "Ubah Password"}
|
|
@@ -150,7 +151,7 @@ export function AccountSettings() {
|
|
| 150 |
placeholder="nama@email.com"
|
| 151 |
/>
|
| 152 |
</div>
|
| 153 |
-
<Button type="submit" disabled={isChangingEmail || !newEmail}>
|
| 154 |
{isChangingEmail ? "Menyimpan..." : "Ubah Email"}
|
| 155 |
</Button>
|
| 156 |
</form>
|
|
|
|
| 20 |
|
| 21 |
const handleChangePassword = async (e: React.FormEvent) => {
|
| 22 |
e.preventDefault();
|
| 23 |
+
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(newPassword)) {
|
| 24 |
+
toast.error("Password minimal 8 karakter, mengandung huruf besar, huruf kecil, dan angka");
|
| 25 |
return;
|
| 26 |
}
|
| 27 |
if (newPassword !== confirmPassword) {
|
|
|
|
| 123 |
</div>
|
| 124 |
<Button
|
| 125 |
type="submit"
|
| 126 |
+
className="rounded-[var(--radius-lg)]"
|
| 127 |
disabled={isChangingPassword || !currentPassword || !newPassword || !confirmPassword}
|
| 128 |
>
|
| 129 |
{isChangingPassword ? "Menyimpan..." : "Ubah Password"}
|
|
|
|
| 151 |
placeholder="nama@email.com"
|
| 152 |
/>
|
| 153 |
</div>
|
| 154 |
+
<Button type="submit" className="rounded-[var(--radius-lg)]" disabled={isChangingEmail || !newEmail}>
|
| 155 |
{isChangingEmail ? "Menyimpan..." : "Ubah Email"}
|
| 156 |
</Button>
|
| 157 |
</form>
|
apps/web/src/components/sign-in-form.tsx
CHANGED
|
@@ -130,7 +130,7 @@ export default function SignInForm({ onSwitchToSignUp }: { onSwitchToSignUp: ()
|
|
| 130 |
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
| 131 |
>
|
| 132 |
{({ canSubmit, isSubmitting }) => (
|
| 133 |
-
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
| 134 |
{isSubmitting ? "Submitting..." : "Sign In"}
|
| 135 |
</Button>
|
| 136 |
)}
|
|
|
|
| 130 |
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
| 131 |
>
|
| 132 |
{({ canSubmit, isSubmitting }) => (
|
| 133 |
+
<Button type="submit" className="w-full rounded-[var(--radius-lg)]" disabled={!canSubmit || isSubmitting}>
|
| 134 |
{isSubmitting ? "Submitting..." : "Sign In"}
|
| 135 |
</Button>
|
| 136 |
)}
|
apps/web/src/components/sign-up-form.tsx
CHANGED
|
@@ -54,7 +54,7 @@ export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: ()
|
|
| 54 |
onSubmit: z.object({
|
| 55 |
name: z.string().min(2, "Name must be at least 2 characters"),
|
| 56 |
email: z.email("Invalid email address"),
|
| 57 |
-
password: z.string().
|
| 58 |
}),
|
| 59 |
},
|
| 60 |
});
|
|
@@ -150,7 +150,7 @@ export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: ()
|
|
| 150 |
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
| 151 |
>
|
| 152 |
{({ canSubmit, isSubmitting }) => (
|
| 153 |
-
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
| 154 |
{isSubmitting ? "Submitting..." : "Sign Up"}
|
| 155 |
</Button>
|
| 156 |
)}
|
|
|
|
| 54 |
onSubmit: z.object({
|
| 55 |
name: z.string().min(2, "Name must be at least 2 characters"),
|
| 56 |
email: z.email("Invalid email address"),
|
| 57 |
+
password: z.string().regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/, "Password must be at least 8 characters with uppercase, lowercase, and number"),
|
| 58 |
}),
|
| 59 |
},
|
| 60 |
});
|
|
|
|
| 150 |
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
| 151 |
>
|
| 152 |
{({ canSubmit, isSubmitting }) => (
|
| 153 |
+
<Button type="submit" className="w-full rounded-[var(--radius-lg)]" disabled={!canSubmit || isSubmitting}>
|
| 154 |
{isSubmitting ? "Submitting..." : "Sign Up"}
|
| 155 |
</Button>
|
| 156 |
)}
|
apps/web/src/routes/__root.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import { Sidebar } from "@/components/sidebar";
|
|
| 8 |
import { ThemeProvider } from "@/components/theme-provider";
|
| 9 |
import { useSidebar } from "@/hooks/use-sidebar";
|
| 10 |
import { GlobalGenerationProgress } from "@/components/generate/GlobalGenerationProgress";
|
| 11 |
-
import { TourGuide } from "@/components/TourGuide";
|
| 12 |
import type { trpc } from "@/utils/trpc";
|
| 13 |
|
| 14 |
import "../index.css";
|
|
@@ -75,7 +74,6 @@ function RootComponent() {
|
|
| 75 |
</div>
|
| 76 |
)}
|
| 77 |
<GlobalGenerationProgress />
|
| 78 |
-
<TourGuide />
|
| 79 |
<Toaster richColors />
|
| 80 |
</ThemeProvider>
|
| 81 |
<TanStackRouterDevtools position="bottom-left" />
|
|
|
|
| 8 |
import { ThemeProvider } from "@/components/theme-provider";
|
| 9 |
import { useSidebar } from "@/hooks/use-sidebar";
|
| 10 |
import { GlobalGenerationProgress } from "@/components/generate/GlobalGenerationProgress";
|
|
|
|
| 11 |
import type { trpc } from "@/utils/trpc";
|
| 12 |
|
| 13 |
import "../index.css";
|
|
|
|
| 74 |
</div>
|
| 75 |
)}
|
| 76 |
<GlobalGenerationProgress />
|
|
|
|
| 77 |
<Toaster richColors />
|
| 78 |
</ThemeProvider>
|
| 79 |
<TanStackRouterDevtools position="bottom-left" />
|
apps/web/src/routes/forgot-password.tsx
CHANGED
|
@@ -133,8 +133,8 @@ function StepReset({ email }: { email: string }) {
|
|
| 133 |
toast.error("Masukkan 6 digit kode OTP");
|
| 134 |
return;
|
| 135 |
}
|
| 136 |
-
if (
|
| 137 |
-
toast.error("Password minimal 8 karakter");
|
| 138 |
return;
|
| 139 |
}
|
| 140 |
if (newPassword !== confirmPassword) {
|
|
|
|
| 133 |
toast.error("Masukkan 6 digit kode OTP");
|
| 134 |
return;
|
| 135 |
}
|
| 136 |
+
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(newPassword)) {
|
| 137 |
+
toast.error("Password minimal 8 karakter, mengandung huruf besar, huruf kecil, dan angka");
|
| 138 |
return;
|
| 139 |
}
|
| 140 |
if (newPassword !== confirmPassword) {
|
apps/web/src/routes/index.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { trpc } from "@/utils/trpc";
|
|
| 5 |
import { Button } from "@labas/ui/components/button";
|
| 6 |
import { Card, CardContent } from "@labas/ui/components/card";
|
| 7 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
|
|
|
| 8 |
|
| 9 |
export const Route = createFileRoute("/")({
|
| 10 |
component: HomeComponent,
|
|
@@ -384,6 +385,8 @@ function HomeComponent() {
|
|
| 384 |
</Link>
|
| 385 |
</div>
|
| 386 |
)}
|
|
|
|
|
|
|
| 387 |
</div>
|
| 388 |
);
|
| 389 |
}
|
|
|
|
| 5 |
import { Button } from "@labas/ui/components/button";
|
| 6 |
import { Card, CardContent } from "@labas/ui/components/card";
|
| 7 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 8 |
+
import { TourGuide } from "@/components/TourGuide";
|
| 9 |
|
| 10 |
export const Route = createFileRoute("/")({
|
| 11 |
component: HomeComponent,
|
|
|
|
| 385 |
</Link>
|
| 386 |
</div>
|
| 387 |
)}
|
| 388 |
+
|
| 389 |
+
<TourGuide />
|
| 390 |
</div>
|
| 391 |
);
|
| 392 |
}
|
apps/web/src/routes/me.tsx
CHANGED
|
@@ -222,6 +222,19 @@ function MeComponent() {
|
|
| 222 |
</CardContent>
|
| 223 |
</Card>
|
| 224 |
</Link>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
</div>
|
| 226 |
</div>
|
| 227 |
);
|
|
|
|
| 222 |
</CardContent>
|
| 223 |
</Card>
|
| 224 |
</Link>
|
| 225 |
+
<Link to="/settings" search={{ tab: "account" }} className="block">
|
| 226 |
+
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[var(--radius-xl)]">
|
| 227 |
+
<CardContent className="p-5 flex items-center gap-4">
|
| 228 |
+
<div className="w-10 h-10 rounded-[var(--radius-lg)] bg-[var(--clay-black)] flex items-center justify-center">
|
| 229 |
+
<MaterialIcon name="settings" className="text-[var(--pure-white)]" />
|
| 230 |
+
</div>
|
| 231 |
+
<div>
|
| 232 |
+
<h3 className="font-semibold text-[var(--clay-black)]">Pengaturan Akun</h3>
|
| 233 |
+
<p className="text-xs text-[var(--warm-charcoal)]">Ubah password, email, dan lainnya</p>
|
| 234 |
+
</div>
|
| 235 |
+
</CardContent>
|
| 236 |
+
</Card>
|
| 237 |
+
</Link>
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
);
|
packages/api/src/routers/verification.ts
CHANGED
|
@@ -122,7 +122,7 @@ export const verificationRouter = router({
|
|
| 122 |
z.object({
|
| 123 |
email: z.string().email(),
|
| 124 |
otp: z.string().length(6),
|
| 125 |
-
newPassword: z.string().
|
| 126 |
}),
|
| 127 |
)
|
| 128 |
.mutation(async ({ input }) => {
|
|
|
|
| 122 |
z.object({
|
| 123 |
email: z.string().email(),
|
| 124 |
otp: z.string().length(6),
|
| 125 |
+
newPassword: z.string().regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/, "Password must be at least 8 characters with uppercase, lowercase, and number"),
|
| 126 |
}),
|
| 127 |
)
|
| 128 |
.mutation(async ({ input }) => {
|