feat: add tg api
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s
This commit is contained in:
18
src/api/api.ts
Normal file
18
src/api/api.ts
Normal 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
1
src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as api } from "./api";
|
||||
@@ -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 }}
|
||||
|
||||
2
src/i18next.d.ts → src/i18n/index.d.ts
vendored
2
src/i18next.d.ts → src/i18n/index.d.ts
vendored
@@ -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
31
src/i18n/index.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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" },
|
||||
|
||||
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Bold.ttf
Normal file
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Bold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-BoldItalic.ttf
Normal file
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Italic.ttf
Normal file
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Italic.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Regular.ttf
Normal file
BIN
src/styles/fonts/BalsamiqSans/BalsamiqSans-Regular.ttf
Normal file
Binary file not shown.
@@ -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;
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "./fonts/BalsamiqSans.css";
|
||||
@import "./fonts/BalsamiqSans";
|
||||
|
||||
@theme {
|
||||
}
|
||||
|
||||
52
src/tg/index.ts
Normal file
52
src/tg/index.ts
Normal 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
3
src/vite-env.d.ts
vendored
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user