feat: add tg api
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s

This commit is contained in:
Hewston Fox
2026-03-16 00:50:53 +02:00
parent 67c2721cff
commit 9f0ff8c4e5
29 changed files with 544 additions and 40 deletions

18
src/api/api.ts Normal file
View File

@@ -0,0 +1,18 @@
import axios from "axios";
import tg, { STORAGE_KEYS } from "@/tg";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
headers: {
...(import.meta.env.DEV && {
"ngrok-skip-browser-warning": "true",
}),
},
});
api.interceptors.request.use(async (config) => {
config.headers.Authorization = `Bearer ${await tg.storage.getItem(STORAGE_KEYS.authToken)}`;
return config;
});
export default api;

1
src/api/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as api } from "./api";

View File

@@ -1,6 +1,8 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import tg from "@/tg";
import classes from "./Button.module.css";
type Props = Omit<HTMLMotionProps<"button">, "className"> & {
@@ -15,10 +17,14 @@ const VARIANTS_MAP = {
yellow: classes.yellowButton,
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
export default function Button({ className, variant = "blue", ...props }: Props) {
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
return (
<motion.button
{...props}
onClick={(e) => {
tg.hapticFeedback.click();
onClick?.(e);
}}
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
whileTap={{ scale: 0.95 }}

View File

@@ -1,6 +1,6 @@
import "i18next";
import type { resources } from "../public/locales/en.d.ts";
import type { resources } from "../../public/locales/en.d.ts";
declare module "i18next" {
interface CustomTypeOptions {

31
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import i18next from "i18next";
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
declare const __LANGS__: string[];
declare const __LOCALES_PATH__: string;
declare const __DEFAULT_LANG__: string;
export const languages = __LANGS__.map((key) => ({
key,
label: key.toUpperCase(),
}));
export default {
init() {
i18next
.use(Backend)
.use(initReactI18next)
.init<HttpBackendOptions>({
backend: {
loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`,
},
fallbackLng: __DEFAULT_LANG__,
supportedLngs: __LANGS__,
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
});
},
};

View File

@@ -1,27 +0,0 @@
import i18next from "i18next";
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
declare const __LANGS__: string[];
declare const __LOCALES_PATH__: string;
declare const __DEFAULT_LANG__: string;
export const languages = __LANGS__.map((key) => ({
key,
label: key.toUpperCase(),
}));
i18next
.use(Backend)
.use(initReactI18next)
.init<HttpBackendOptions>({
backend: {
loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`,
},
fallbackLng: __DEFAULT_LANG__,
supportedLngs: __LANGS__,
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
});

View File

@@ -3,9 +3,11 @@ import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./styles/index.css";
import i18n from "@/i18n";
import tg from "@/tg";
import { routeTree } from "./routeTree.gen";
import "./i18next";
import "./styles/index.css";
const router = createRouter({ routeTree });
@@ -16,6 +18,9 @@ declare module "@tanstack/react-router" {
}
}
tg.init();
i18n.init();
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={new QueryClient()}>

View File

@@ -15,7 +15,7 @@ import SwitchInput from "@components/form/SwitchInput";
import TextInput from "@components/form/TextInput";
import NumberInput from "@components/form/NumberInput";
import TextAreaInput from "@components/form/TextAreaInput";
import ActionModal from "@components/modal/ActionModal";
import ActionModal from "@components/modals/ActionModal";
const TABS = [
{ key: "tab1", title: "Tab 1" },

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Regular.ttf") format("truetype");
src: url("./BalsamiqSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -8,7 +8,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Italic.ttf") format("truetype");
src: url("./BalsamiqSans-Italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
@@ -16,7 +16,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Bold.ttf") format("truetype");
src: url("./BalsamiqSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -24,7 +24,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-BoldItalic.ttf") format("truetype");
src: url("./BalsamiqSans-BoldItalic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
font-display: swap;

View File

@@ -1,6 +1,6 @@
@import "tailwindcss";
@import "./fonts/BalsamiqSans.css";
@import "./fonts/BalsamiqSans";
@theme {
}

52
src/tg/index.ts Normal file
View File

@@ -0,0 +1,52 @@
import * as tg from "@telegram-apps/sdk-react";
export const STORAGE_KEYS = {
authToken: "authToken",
} as const;
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
export default {
init: () => {
try {
tg.setDebug(import.meta.env.DEV);
tg.init({ acceptCustomStyles: true });
tg.requestFullscreen();
tg.disableVerticalSwipes();
tg.expandViewport();
tg.setMiniAppHeaderColor("#000000");
} catch {
console.warn("Telegram SDK not available in browser.");
}
},
openLink(url: string | URL, options?: tg.OpenLinkOptions) {
tg.openLink.ifAvailable(url, options);
},
hapticFeedback: {
click() {
tg.hapticFeedback.impactOccurred.ifAvailable("light");
},
},
initData: tg.initData,
storage: {
clear() {
localStorage.clear();
tg.cloudStorage.clear.ifAvailable();
},
getItem(key: StorageKey, options?: tg.InvokeCustomMethodOptions) {
return tg.cloudStorage
.getItem(key, options)
.catch(
() =>
localStorage.getItem(key) ?? tg.AbortablePromise.reject(new Error("Item not found")),
);
},
setItem(key: StorageKey, value: string, options?: tg.InvokeCustomMethodOptions) {
localStorage.setItem(key, value);
tg.cloudStorage.setItem.ifAvailable(key, value, options);
},
deleteItem(key: StorageKey, options?: tg.InvokeCustomMethodOptions) {
tg.cloudStorage.deleteItem.ifAvailable(key, options);
localStorage.removeItem(key);
},
},
};

3
src/vite-env.d.ts vendored
View File

@@ -4,6 +4,9 @@ interface ViteTypeOptions {
interface ImportMetaEnv {
MODE: "development" | "production" | "staging";
VITE_APP_NAME: string;
VITE_APP_URL: string;
VITE_API_BASE_URL: string;
}
interface ImportMeta {