chore: prepare for migration to OpenAPI
This commit is contained in:
@@ -18,6 +18,14 @@
|
|||||||
"recommended": true
|
"recommended": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": ["src/routeTree.gen.ts", "dist/**", "node_modules/**"],
|
||||||
|
"linter": { "enabled": false },
|
||||||
|
"formatter": { "enabled": false },
|
||||||
|
"assist": { "enabled": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"quoteStyle": "double"
|
"quoteStyle": "double"
|
||||||
|
|||||||
36
src/api/index.tsx
Normal file
36
src/api/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { cors } from "@elysiajs/cors";
|
||||||
|
import { swagger } from "@elysiajs/swagger";
|
||||||
|
import Elysia from "elysia";
|
||||||
|
import { apiMiddleware } from "../middleware/apiMiddleware";
|
||||||
|
import { auth } from "../utils/auth";
|
||||||
|
import { apikey } from "./apikey";
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
const api = new Elysia({
|
||||||
|
prefix: "/api",
|
||||||
|
})
|
||||||
|
.use(cors())
|
||||||
|
.all("/auth/*", ({ request }) => auth.handler(request))
|
||||||
|
.get("/session", async ({ request }) => {
|
||||||
|
const data = await auth.api.getSession({ headers: request.headers });
|
||||||
|
return { data };
|
||||||
|
})
|
||||||
|
.use(apiMiddleware)
|
||||||
|
.use(apikey);
|
||||||
|
|
||||||
|
if (!isProduction) {
|
||||||
|
api.use(
|
||||||
|
swagger({
|
||||||
|
path: "/docs",
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "Bun + React API",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api;
|
||||||
@@ -8,12 +8,12 @@
|
|||||||
/** biome-ignore-all lint/suspicious/noAssignInExpressions: <explanation */
|
/** biome-ignore-all lint/suspicious/noAssignInExpressions: <explanation */
|
||||||
|
|
||||||
import { createTheme, MantineProvider } from "@mantine/core";
|
import { createTheme, MantineProvider } from "@mantine/core";
|
||||||
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
import { Inspector } from "react-dev-inspector";
|
import { Inspector } from "react-dev-inspector";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { ModalsProvider } from "@mantine/modals";
|
import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
|
||||||
import { VITE_PUBLIC_URL, IS_DEV } from "./utils/env";
|
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
@@ -40,12 +40,11 @@ const elem = document.getElementById("root")!;
|
|||||||
const app = (
|
const app = (
|
||||||
<InspectorWrapper
|
<InspectorWrapper
|
||||||
keys={["shift", "a"]}
|
keys={["shift", "a"]}
|
||||||
onClickElement={(e) => {
|
onClickElement={(e) => {
|
||||||
if (!e.codeInfo) return;
|
if (!e.codeInfo) return;
|
||||||
|
|
||||||
const url = VITE_PUBLIC_URL;
|
const url = VITE_PUBLIC_URL;
|
||||||
fetch(`${url}/__open-in-editor`, {
|
fetch(`${url}/__open-in-editor`, {
|
||||||
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -182,6 +182,6 @@ code {
|
|||||||
*,
|
*,
|
||||||
::before,
|
::before,
|
||||||
::after {
|
::after {
|
||||||
animation: none !important;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/index.ts
42
src/index.ts
@@ -1,42 +1,13 @@
|
|||||||
/** biome-ignore-all lint/suspicious/noExplicitAny: penjelasannya */
|
/** biome-ignore-all lint/suspicious/noExplicitAny: penjelasannya */
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cors } from "@elysiajs/cors";
|
|
||||||
import { swagger } from "@elysiajs/swagger";
|
|
||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
import { apikey } from "./api/apikey";
|
import api from "./api";
|
||||||
import { apiMiddleware } from "./middleware/apiMiddleware";
|
|
||||||
import { auth } from "./utils/auth";
|
|
||||||
import { openInEditor } from "./utils/open-in-editor";
|
import { openInEditor } from "./utils/open-in-editor";
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
const api = new Elysia({
|
|
||||||
prefix: "/api",
|
|
||||||
})
|
|
||||||
.use(cors())
|
|
||||||
.all("/auth/*", ({ request }) => auth.handler(request))
|
|
||||||
.get("/session", async ({ request }) => {
|
|
||||||
const data = await auth.api.getSession({ headers: request.headers });
|
|
||||||
return { data };
|
|
||||||
})
|
|
||||||
.use(apiMiddleware)
|
|
||||||
.use(apikey);
|
|
||||||
|
|
||||||
if (!isProduction) {
|
|
||||||
api.use(
|
|
||||||
swagger({
|
|
||||||
path: "/docs",
|
|
||||||
documentation: {
|
|
||||||
info: {
|
|
||||||
title: "Bun + React API",
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Elysia().use(api);
|
const app = new Elysia().use(api);
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
@@ -158,8 +129,11 @@ if (!isProduction) {
|
|||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
// 1. Try exact match in dist
|
// 1. Try exact match in dist
|
||||||
let filePath = path.join("dist", pathname === "/" ? "index.html" : pathname);
|
let filePath = path.join(
|
||||||
|
"dist",
|
||||||
|
pathname === "/" ? "index.html" : pathname,
|
||||||
|
);
|
||||||
|
|
||||||
// 2. If not found and looks like an asset (has extension), try root of dist
|
// 2. If not found and looks like an asset (has extension), try root of dist
|
||||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||||
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
||||||
@@ -191,4 +165,4 @@ console.log(
|
|||||||
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`,
|
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`,
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ApiApp = typeof app;
|
export type ApiApp = typeof app;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
AppShell,
|
AppShell,
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -31,8 +30,8 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import { authStore } from "../../store/auth";
|
|
||||||
import { authClient } from "@/utils/auth-client";
|
import { authClient } from "@/utils/auth-client";
|
||||||
|
import { authStore } from "../../store/auth";
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard")({
|
export const Route = createFileRoute("/dashboard")({
|
||||||
component: DashboardLayout,
|
component: DashboardLayout,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
|
|
||||||
import { authClient } from "@/utils/auth-client";
|
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -13,11 +11,11 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
|
rem,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
rem,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +32,8 @@ import {
|
|||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
|
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
|
||||||
|
import { authClient } from "@/utils/auth-client";
|
||||||
import { authStore } from "../store/auth";
|
import { authStore } from "../store/auth";
|
||||||
|
|
||||||
export const Route = createFileRoute("/profile")({
|
export const Route = createFileRoute("/profile")({
|
||||||
@@ -62,7 +62,8 @@ function Profile() {
|
|||||||
size: "sm",
|
size: "sm",
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Apakah Anda yakin ingin keluar dari akun Anda? Anda harus masuk kembali untuk mengakses data Anda.
|
Apakah Anda yakin ingin keluar dari akun Anda? Anda harus masuk
|
||||||
|
kembali untuk mengakses data Anda.
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: { confirm: "Keluar", cancel: "Batal" },
|
labels: { confirm: "Keluar", cancel: "Batal" },
|
||||||
@@ -78,8 +79,20 @@ function Profile() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfoField = ({ icon: Icon, label, value, copyable = false, id = "" }: any) => (
|
const InfoField = ({
|
||||||
<Paper withBorder p="md" radius="md" bg="rgba(251, 240, 223, 0.03)" style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}>
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
copyable = false,
|
||||||
|
id = "",
|
||||||
|
}: any) => (
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
bg="rgba(251, 240, 223, 0.03)"
|
||||||
|
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||||
|
>
|
||||||
<Group wrap="nowrap" align="flex-start">
|
<Group wrap="nowrap" align="flex-start">
|
||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<Icon size={20} stroke={1.5} color="#f3d5a3" />
|
<Icon size={20} stroke={1.5} color="#f3d5a3" />
|
||||||
@@ -89,18 +102,32 @@ function Profile() {
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs" mt={4} wrap="nowrap">
|
<Group gap="xs" mt={4} wrap="nowrap">
|
||||||
<Text fw={500} size="sm" c="#fbf0df" truncate="end" style={{ flex: 1 }}>
|
<Text
|
||||||
|
fw={500}
|
||||||
|
size="sm"
|
||||||
|
c="#fbf0df"
|
||||||
|
truncate="end"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
{value || "N/A"}
|
{value || "N/A"}
|
||||||
</Text>
|
</Text>
|
||||||
{copyable && value && (
|
{copyable && value && (
|
||||||
<Tooltip label={copied === id ? "Copied!" : "Salin ke papan klip"} position="top" withArrow>
|
<Tooltip
|
||||||
|
label={copied === id ? "Copied!" : "Salin ke papan klip"}
|
||||||
|
position="top"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color={copied === id ? "green" : "gray"}
|
color={copied === id ? "green" : "gray"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => copyToClipboard(value, id)}
|
onClick={() => copyToClipboard(value, id)}
|
||||||
>
|
>
|
||||||
{copied === id ? <IconCheck size={14} /> : <IconCopy size={14} />}
|
{copied === id ? (
|
||||||
|
<IconCheck size={14} />
|
||||||
|
) : (
|
||||||
|
<IconCopy size={14} />
|
||||||
|
)}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -116,8 +143,12 @@ function Profile() {
|
|||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={1} c="#f3d5a3">Profil Saya</Title>
|
<Title order={1} c="#f3d5a3">
|
||||||
<Text c="dimmed" size="sm">Kelola informasi akun dan pengaturan keamanan Anda</Text>
|
Profil Saya
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
Kelola informasi akun dan pengaturan keamanan Anda
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Group>
|
<Group>
|
||||||
{snap.user?.role === "admin" && (
|
{snap.user?.role === "admin" && (
|
||||||
@@ -144,24 +175,47 @@ function Profile() {
|
|||||||
<Divider color="rgba(251, 240, 223, 0.1)" />
|
<Divider color="rgba(251, 240, 223, 0.1)" />
|
||||||
|
|
||||||
{/* Profile Overview Card */}
|
{/* Profile Overview Card */}
|
||||||
<Card withBorder radius="lg" p={0} bg="rgba(26, 26, 26, 0.5)" style={{ overflow: "hidden" }}>
|
<Card
|
||||||
<Box h={120} bg="linear-gradient(45deg, #2c2c2c 0%, #1a1a1a 100%)" style={{ borderBottom: "1px solid rgba(251, 240, 223, 0.1)" }} />
|
withBorder
|
||||||
|
radius="lg"
|
||||||
|
p={0}
|
||||||
|
bg="rgba(26, 26, 26, 0.5)"
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
h={120}
|
||||||
|
bg="linear-gradient(45deg, #2c2c2c 0%, #1a1a1a 100%)"
|
||||||
|
style={{ borderBottom: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||||
|
/>
|
||||||
<Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}>
|
<Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}>
|
||||||
<Group align="flex-end" gap="xl" mb="md">
|
<Group align="flex-end" gap="xl" mb="md">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={snap.user?.image}
|
src={snap.user?.image}
|
||||||
size={120}
|
size={120}
|
||||||
radius={120}
|
radius={120}
|
||||||
style={{ border: "4px solid #1a1a1a", boxShadow: "0 4px 10px rgba(0,0,0,0.3)" }}
|
style={{
|
||||||
|
border: "4px solid #1a1a1a",
|
||||||
|
boxShadow: "0 4px 10px rgba(0,0,0,0.3)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{snap.user?.name?.charAt(0).toUpperCase()}
|
{snap.user?.name?.charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Stack gap={0} pb="md">
|
<Stack gap={0} pb="md">
|
||||||
<Title order={2} c="#fbf0df">{snap.user?.name}</Title>
|
<Title order={2} c="#fbf0df">
|
||||||
|
{snap.user?.name}
|
||||||
|
</Title>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Text c="dimmed" size="sm">{snap.user?.email}</Text>
|
<Text c="dimmed" size="sm">
|
||||||
<Text c="dimmed" size="xs">•</Text>
|
{snap.user?.email}
|
||||||
<Badge variant="dot" color={snap.user?.role === "admin" ? "orange" : "blue"} size="sm">
|
</Text>
|
||||||
|
<Text c="dimmed" size="xs">
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
variant="dot"
|
||||||
|
color={snap.user?.role === "admin" ? "orange" : "blue"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
{snap.user?.role || "user"}
|
{snap.user?.role || "user"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -173,19 +227,41 @@ function Profile() {
|
|||||||
<Grid gutter="lg">
|
<Grid gutter="lg">
|
||||||
<Grid.Col span={{ base: 12, md: 7 }}>
|
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={4} c="#f3d5a3">Informasi Identitas</Title>
|
<Title order={4} c="#f3d5a3">
|
||||||
|
Informasi Identitas
|
||||||
|
</Title>
|
||||||
<Grid gutter="sm">
|
<Grid gutter="sm">
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<InfoField icon={IconUser} label="Nama Lengkap" value={snap.user?.name} />
|
<InfoField
|
||||||
|
icon={IconUser}
|
||||||
|
label="Nama Lengkap"
|
||||||
|
value={snap.user?.name}
|
||||||
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<InfoField icon={IconShield} label="Peran" value={snap.user?.role || "User"} />
|
<InfoField
|
||||||
|
icon={IconShield}
|
||||||
|
label="Peran"
|
||||||
|
value={snap.user?.role || "User"}
|
||||||
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={12}>
|
<Grid.Col span={12}>
|
||||||
<InfoField icon={IconAt} label="Alamat Email" value={snap.user?.email} copyable id="email" />
|
<InfoField
|
||||||
|
icon={IconAt}
|
||||||
|
label="Alamat Email"
|
||||||
|
value={snap.user?.email}
|
||||||
|
copyable
|
||||||
|
id="email"
|
||||||
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={12}>
|
<Grid.Col span={12}>
|
||||||
<InfoField icon={IconId} label="Unique User ID" value={snap.user?.id} copyable id="userid" />
|
<InfoField
|
||||||
|
icon={IconId}
|
||||||
|
label="Unique User ID"
|
||||||
|
value={snap.user?.id}
|
||||||
|
copyable
|
||||||
|
id="userid"
|
||||||
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -193,40 +269,73 @@ function Profile() {
|
|||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={4} c="#f3d5a3">Keamanan & Sesi</Title>
|
<Title order={4} c="#f3d5a3">
|
||||||
<Card withBorder radius="md" p="lg" bg="rgba(251, 240, 223, 0.03)" style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}>
|
Keamanan & Sesi
|
||||||
|
</Title>
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
bg="rgba(251, 240, 223, 0.03)"
|
||||||
|
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||||
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>Sesi Saat Ini</Text>
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
|
||||||
|
Sesi Saat Ini
|
||||||
|
</Text>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Badge color="green" variant="light">Aktif Sekarang</Badge>
|
<Badge color="green" variant="light">
|
||||||
<Text size="xs" c="dimmed">ID: {snap.session?.id?.substring(0, 8)}...</Text>
|
Aktif Sekarang
|
||||||
|
</Badge>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
ID: {snap.session?.id?.substring(0, 8)}...
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>Session Token</Text>
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
|
||||||
|
Session Token
|
||||||
|
</Text>
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Code block style={{
|
<Code
|
||||||
backgroundColor: "rgba(0,0,0,0.3)",
|
block
|
||||||
color: "#f3d5a3",
|
style={{
|
||||||
border: "1px solid rgba(251, 240, 223, 0.1)",
|
backgroundColor: "rgba(0,0,0,0.3)",
|
||||||
fontSize: rem(11),
|
color: "#f3d5a3",
|
||||||
flex: 1
|
border: "1px solid rgba(251, 240, 223, 0.1)",
|
||||||
}}>
|
fontSize: rem(11),
|
||||||
{snap.session?.token ? `${snap.session.token.substring(0, 32)}...` : "N/A"}
|
flex: 1,
|
||||||
</Code>
|
}}
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
onClick={() => snap.session?.token && copyToClipboard(snap.session.token, "token")}
|
|
||||||
>
|
>
|
||||||
{copied === "token" ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
{snap.session?.token
|
||||||
|
? `${snap.session.token.substring(0, 32)}...`
|
||||||
|
: "N/A"}
|
||||||
|
</Code>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
onClick={() =>
|
||||||
|
snap.session?.token &&
|
||||||
|
copyToClipboard(snap.session.token, "token")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copied === "token" ? (
|
||||||
|
<IconCheck size={16} />
|
||||||
|
) : (
|
||||||
|
<IconCopy size={16} />
|
||||||
|
)}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button variant="light" color="gray" fullWidth leftSection={<IconExternalLink size={16} />}>
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
fullWidth
|
||||||
|
leftSection={<IconExternalLink size={16} />}
|
||||||
|
>
|
||||||
Riwayat Sesi
|
Riwayat Sesi
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export const getEnv = (key: string, defaultValue = ""): string => {
|
export const getEnv = (key: string, defaultValue = ""): string => {
|
||||||
// 1. Try Vite's import.meta.env
|
// 1. Try Vite's import.meta.env
|
||||||
try {
|
try {
|
||||||
if (typeof import.meta.env !== "undefined" && import.meta.env[key]) {
|
if (import.meta.env?.[key]) {
|
||||||
return import.meta.env[key];
|
return import.meta.env[key];
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -20,11 +20,14 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VITE_PUBLIC_URL = getEnv("VITE_PUBLIC_URL", "http://localhost:3000");
|
export const VITE_PUBLIC_URL = getEnv(
|
||||||
|
"VITE_PUBLIC_URL",
|
||||||
|
"http://localhost:3000",
|
||||||
|
);
|
||||||
|
|
||||||
export const IS_DEV = (() => {
|
export const IS_DEV = (() => {
|
||||||
try {
|
try {
|
||||||
return typeof import.meta.env !== "undefined" && import.meta.env.DEV;
|
return import.meta.env?.DEV;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// open-in-editor.ts
|
// open-in-editor.ts
|
||||||
// DEV utility: open source file in local editor
|
// DEV utility: open source file in local editor
|
||||||
|
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "node:child_process";
|
||||||
import fs from "fs";
|
import fs from "node:fs";
|
||||||
import path from "path";
|
import path from "node:path";
|
||||||
|
|
||||||
/* -------------------------------------------------------
|
/* -------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
|
|||||||
Reference in New Issue
Block a user