├── globals.d.ts ├── .eslintignore ├── shared ├── Button │ ├── index.tsx │ ├── Button.module.css │ ├── usePulseEffect.tsx │ └── Button.tsx ├── Popover │ ├── index.tsx │ ├── useClickOutside.ts │ └── Popover.tsx ├── Icon │ ├── resolver.ts │ └── index.tsx ├── Switch.tsx ├── Media.tsx ├── Form.tsx └── Input.tsx ├── components ├── Header │ ├── index.tsx │ └── Header.tsx ├── SeoHead │ ├── index.tsx │ └── SeoHead.tsx ├── MobileMenu │ ├── index.tsx │ ├── MobileMenu.tsx │ └── MenuList.tsx ├── Pages │ ├── AdminPage │ │ ├── Table │ │ │ ├── index.tsx │ │ │ └── Table.tsx │ │ ├── index.tsx │ │ ├── TableRow │ │ │ ├── index.tsx │ │ │ └── TableRow.tsx │ │ ├── TableHead │ │ │ ├── index.tsx │ │ │ └── TableHead.tsx │ │ ├── BotSetting │ │ │ ├── index.tsx │ │ │ ├── inputValidation.ts │ │ │ ├── BotSetting.tsx │ │ │ └── BotForm.tsx │ │ ├── PageButton │ │ │ ├── index.tsx │ │ │ └── PageButton.tsx │ │ ├── Pagination │ │ │ ├── index.tsx │ │ │ └── Pagination.tsx │ │ ├── SearchInput │ │ │ ├── index.tsx │ │ │ └── SearchInput.tsx │ │ ├── types.ts │ │ ├── AdminPage.tsx │ │ └── SkeletonTableBody.tsx │ └── HomePage │ │ ├── index.tsx │ │ └── HomePage.tsx ├── DesktopMenu │ ├── index.tsx │ └── DesktopMenu.tsx ├── Layout │ ├── types.ts │ ├── index.tsx │ └── BaseLayout.tsx └── GoogleLoginButton │ ├── index.tsx │ └── GoogleLoginButton.tsx ├── .prettierrc ├── postcss.config.js ├── public ├── images │ ├── seo_logo.png │ ├── Header │ │ ├── logo.jpg │ │ ├── logo.png │ │ └── menu_icon.png │ └── Home │ │ ├── d_weather_bg.jpg │ │ └── d_weather_dark_bg.jpg └── fonts │ └── Poppins │ ├── Poppins-Black.ttf │ ├── Poppins-Bold.ttf │ ├── Poppins-Light.ttf │ ├── Poppins-Thin.ttf │ ├── Poppins-Italic.ttf │ ├── Poppins-Medium.ttf │ ├── Poppins-Regular.ttf │ ├── Poppins-SemiBold.ttf │ ├── Poppins-BoldItalic.ttf │ ├── Poppins-ExtraBold.ttf │ ├── Poppins-ExtraLight.ttf │ ├── Poppins-ThinItalic.ttf │ ├── Poppins-BlackItalic.ttf │ ├── Poppins-LightItalic.ttf │ ├── Poppins-MediumItalic.ttf │ ├── Poppins-ExtraBoldItalic.ttf │ ├── Poppins-SemiBoldItalic.ttf │ ├── Poppins-ExtraLightItalic.ttf │ └── OFL.txt ├── pages ├── views │ ├── home.tsx │ └── admin.tsx └── _app.tsx ├── nodemon.json ├── next-env.d.ts ├── hooks └── useIsMobile.ts ├── server ├── AppController.ts ├── bot │ ├── help.ts │ ├── unsubscribe.ts │ ├── rejoin.ts │ ├── subscribe.ts │ ├── start.ts │ ├── WeatherBot.ts │ └── weather.ts ├── model │ ├── BotModel.ts │ ├── UserModel.ts │ └── AdminModel.ts ├── ApplicationModule.ts ├── main.ts └── controller │ ├── AdminController.ts │ ├── UserController.ts │ └── BotController.ts ├── lib ├── getTelegramUsers.ts ├── addAdminData.ts ├── getUserData.ts ├── weatherBot.ts └── telegramUser.ts ├── DTO └── admin.dto.ts ├── utils └── constants │ └── env.ts ├── next.config.js ├── tsconfig.server.json ├── .gitignore ├── README.md ├── providers ├── ThemeProvider.tsx ├── UserProvider.tsx └── AdminProvider.tsx ├── tsconfig.json ├── tailwind.config.js ├── styles └── globals.css ├── .github └── workflows │ ├── lint.yml │ └── lint.yaml ├── .eslintrc.json └── package.json /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .eslintrc.json 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /shared/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from "./Button" 2 | 3 | export default Button 4 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import Header from "./Header" 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /shared/Popover/index.tsx: -------------------------------------------------------------------------------- 1 | import Popover from "./Popover" 2 | 3 | export default Popover 4 | -------------------------------------------------------------------------------- /components/SeoHead/index.tsx: -------------------------------------------------------------------------------- 1 | import SeoHead from "./SeoHead" 2 | 3 | export default SeoHead 4 | -------------------------------------------------------------------------------- /components/MobileMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import MobileMenu from "./MobileMenu" 2 | 3 | export default MobileMenu 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import Table from "./Table" 2 | 3 | export default Table 4 | -------------------------------------------------------------------------------- /components/Pages/HomePage/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from "./HomePage" 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /components/DesktopMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import DesktopMenu from "./DesktopMenu" 2 | 3 | export default DesktopMenu 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/index.tsx: -------------------------------------------------------------------------------- 1 | import AdminPage from "./AdminPage" 2 | 3 | export default AdminPage 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "trailingComma": "all" 6 | } -------------------------------------------------------------------------------- /components/Pages/AdminPage/TableRow/index.tsx: -------------------------------------------------------------------------------- 1 | import TableRow from "./TableRow" 2 | 3 | export default TableRow 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/TableHead/index.tsx: -------------------------------------------------------------------------------- 1 | import TableHead from "./TableHead" 2 | 3 | export default TableHead 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/BotSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import BotSetting from "./BotSetting" 2 | 3 | export default BotSetting 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/PageButton/index.tsx: -------------------------------------------------------------------------------- 1 | import PageButton from "./PageButton" 2 | 3 | export default PageButton 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/Pagination/index.tsx: -------------------------------------------------------------------------------- 1 | import Pagination from "./Pagination" 2 | 3 | export default Pagination 4 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/SearchInput/index.tsx: -------------------------------------------------------------------------------- 1 | import SearchInput from "./SearchInput" 2 | 3 | export default SearchInput 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/seo_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/seo_logo.png -------------------------------------------------------------------------------- /components/Layout/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export interface ILayout { 4 | children: ReactNode 5 | } 6 | -------------------------------------------------------------------------------- /components/GoogleLoginButton/index.tsx: -------------------------------------------------------------------------------- 1 | import GoogleLoginButton from "./GoogleLoginButton" 2 | 3 | export default GoogleLoginButton 4 | -------------------------------------------------------------------------------- /public/images/Header/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/Header/logo.jpg -------------------------------------------------------------------------------- /public/images/Header/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/Header/logo.png -------------------------------------------------------------------------------- /pages/views/home.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from "components/Pages/HomePage" 2 | 3 | const Home = () => 4 | 5 | export default Home 6 | -------------------------------------------------------------------------------- /public/images/Header/menu_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/Header/menu_icon.png -------------------------------------------------------------------------------- /public/images/Home/d_weather_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/Home/d_weather_bg.jpg -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Black.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Thin.ttf -------------------------------------------------------------------------------- /pages/views/admin.tsx: -------------------------------------------------------------------------------- 1 | import AdminPage from "../../components/Pages/AdminPage" 2 | 3 | const Admin = () => 4 | 5 | export default Admin 6 | -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /public/images/Home/d_weather_dark_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/images/Home/d_weather_dark_bg.jpg -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server/**/*"], 3 | "ext": "ts,tsx", 4 | "execMap": { 5 | "ts": "ts-node --project tsconfig.server.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-BoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-ExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-ThinItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-BlackItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-LightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-MediumItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/Poppins/Poppins-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativewebdev21/Telegram-Bot-Weather-UI/HEAD/public/fonts/Poppins/Poppins-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /components/Pages/AdminPage/types.ts: -------------------------------------------------------------------------------- 1 | export type TelegramUser = { 2 | userid: number 3 | username: string 4 | time: string 5 | blocked: boolean 6 | subscribed: boolean 7 | } 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "usehooks-ts" 2 | 3 | const useIsMobile = () => { 4 | const isMobile = useMediaQuery("(max-width: 768px)") 5 | 6 | return isMobile 7 | } 8 | 9 | export default useIsMobile 10 | -------------------------------------------------------------------------------- /server/AppController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from "@nestjs/common" 2 | 3 | @Controller() 4 | class AppController { 5 | @Render("home") 6 | @Get("/") 7 | public index() {} 8 | } 9 | 10 | export default AppController 11 | -------------------------------------------------------------------------------- /shared/Icon/resolver.ts: -------------------------------------------------------------------------------- 1 | import { AiOutlineArrowRight, AiOutlineArrowLeft, AiOutlineClose } from "react-icons/ai" 2 | 3 | export const Icons = { 4 | arrowNext: AiOutlineArrowRight, 5 | arrowPrev: AiOutlineArrowLeft, 6 | close: AiOutlineClose, 7 | } 8 | -------------------------------------------------------------------------------- /lib/getTelegramUsers.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const getTelegramUsers = async () => { 4 | try { 5 | const response = await axios.get("/api/user/all") 6 | 7 | return response.data 8 | } catch (err) { 9 | return { err } 10 | } 11 | } 12 | 13 | export default getTelegramUsers 14 | -------------------------------------------------------------------------------- /DTO/admin.dto.ts: -------------------------------------------------------------------------------- 1 | class AdminDTO { 2 | readonly email: string; 3 | readonly email_verified: boolean 4 | readonly family_name: string 5 | readonly given_name: string 6 | readonly locale: string 7 | readonly name: string 8 | readonly picture: string 9 | readonly sub: number 10 | } 11 | 12 | export default AdminDTO -------------------------------------------------------------------------------- /lib/addAdminData.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const addAdminData = async (userData: any) => { 4 | try { 5 | const response = await axios.post("/api/admin/add", { ...userData }) 6 | 7 | return response 8 | } catch (err) { 9 | return { err } 10 | } 11 | } 12 | 13 | export default addAdminData 14 | -------------------------------------------------------------------------------- /utils/constants/env.ts: -------------------------------------------------------------------------------- 1 | // ./src/shared/constants/env.ts 2 | export const isServer = typeof window === "undefined" 3 | 4 | export const isClient = !isServer 5 | 6 | export const { NODE_ENV } = process.env 7 | 8 | export const PORT = process.env.PORT || 3000 9 | 10 | export const MONGO_URI = "mongodb://localhost:27017/telebot" 11 | -------------------------------------------------------------------------------- /shared/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | /* Pulse Effect */ 2 | .pulse__effect { 3 | animation: pulse-effect 1s ease-out; 4 | } 5 | 6 | @keyframes pulse-effect { 7 | 0% { 8 | opacity: 0.2; 9 | height: 0px; 10 | width: 0px; 11 | } 12 | 100% { 13 | opacity: 0; 14 | height: 400px; 15 | width: 400px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/bot/help.ts: -------------------------------------------------------------------------------- 1 | const help = (bot: any) => { 2 | bot.command("help", (ctx: any) => { 3 | ctx.reply( 4 | "The Bot Commands are as follows:\n\n/subscribe - Subscribe to bot\n/weather - Get the current weather data\n/unsubscribe - Unsubscribe from bot\n/rejoin - Rejoin to bot", 5 | ) 6 | }) 7 | } 8 | 9 | export default help 10 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/BotSetting/inputValidation.ts: -------------------------------------------------------------------------------- 1 | import JoiBase from "joi" 2 | 3 | const inputValidation = JoiBase.object({ 4 | newKey: JoiBase.string().messages({ 5 | "string.empty": `BotKey cannot be an empty field`, 6 | }), 7 | newHandle: JoiBase.string().messages({ 8 | "string.empty": "Handle cannot be an empty field", 9 | }), 10 | }) 11 | 12 | export { inputValidation } 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = { 4 | images: { 5 | domains: ["*"], 6 | }, 7 | webpack: (config) => { 8 | config.resolve.fallback = { fs: false, net: false, tls: false } 9 | return config 10 | }, 11 | reactStrictMode: true, 12 | } 13 | 14 | module.exports = nextConfig 15 | -------------------------------------------------------------------------------- /lib/getUserData.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const getUserData = async (credentialToken: any) => { 4 | try { 5 | const response = await axios.get("https://www.googleapis.com/oauth2/v3/userinfo", { 6 | headers: { Authorization: `Bearer ${credentialToken.access_token}` }, 7 | }) 8 | 9 | return response.data 10 | } catch (err) { 11 | return null 12 | } 13 | } 14 | 15 | export default getUserData 16 | -------------------------------------------------------------------------------- /server/model/BotModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models, Model, Document } from "mongoose" 2 | 3 | interface BotModel extends Document { 4 | key: string 5 | handle: string 6 | } 7 | 8 | const BotSchema = new Schema({ 9 | key: { 10 | type: String, 11 | required: true, 12 | }, 13 | handle: { 14 | type: String, 15 | required: true, 16 | }, 17 | }) 18 | 19 | export default (models.Bot as Model) || model("Bot", BotSchema) 20 | -------------------------------------------------------------------------------- /components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import BaseLayout from "./BaseLayout" 2 | import { ILayout } from "./types" 3 | 4 | const layoutContainers = { 5 | base: BaseLayout, 6 | } 7 | 8 | interface ILayoutFactory extends ILayout { 9 | type: keyof typeof layoutContainers 10 | } 11 | 12 | function Layout({ children, type }: ILayoutFactory) { 13 | const Container = layoutContainers[type] 14 | 15 | return {children} 16 | } 17 | 18 | export default Layout 19 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": ".next/production-server", 5 | "declaration": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "isolatedModules": false, 12 | "noEmit": false, 13 | "allowJs": false 14 | }, 15 | "include": ["./server/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /lib/weatherBot.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | export const getBotKey = async () => { 4 | try { 5 | const response = await axios.get("/api/bot/getKey") 6 | 7 | return response.data 8 | } catch (err) { 9 | return { err } 10 | } 11 | } 12 | 13 | export const restartBot = async (newKey: string, oldKey: string, newHandle: string) => { 14 | try { 15 | const response = await axios.post("/api/bot/relanuch", { newKey, oldKey, newHandle }) 16 | 17 | return response.data 18 | } catch (err) { 19 | return { err } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/BotSetting/BotSetting.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import BotForm from "./BotForm" 3 | 4 | const BotSetting = () => ( 5 |
6 |
11 |

Bot Setting

12 | 13 |
14 |
15 | ) 16 | 17 | export default BotSetting 18 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/TableHead/TableHead.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react" 2 | 3 | interface TableHeadProps { 4 | children: any 5 | className?: string 6 | } 7 | const TableHead: FC = ({ children, className }) => ( 8 | 16 | {children} 17 | 18 | ) 19 | 20 | export default TableHead 21 | -------------------------------------------------------------------------------- /components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import DesktopMenu from "../DesktopMenu" 3 | import MobileMenu from "../MobileMenu" 4 | 5 | const Header = () => ( 6 | 19 | ) 20 | 21 | export default Header 22 | -------------------------------------------------------------------------------- /server/bot/unsubscribe.ts: -------------------------------------------------------------------------------- 1 | import User from "../model/UserModel" 2 | 3 | const unsubscribe = (bot: any) => { 4 | bot.command("unsubscribe", async (ctx: any) => { 5 | const userid = ctx.from.id 6 | 7 | const user = await User.findOneAndUpdate({ userid }, { subscribed: false }) 8 | 9 | if (!user) { 10 | ctx.reply( 11 | "You have not subscribed to get Weather Updates!\nUse /subscribe to subscribe to the bot.", 12 | ) 13 | return 14 | } 15 | 16 | ctx.reply("You have unsubscribed!!!") 17 | }) 18 | } 19 | 20 | export default unsubscribe 21 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/AdminPage.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../../Layout" 2 | import Table from "./Table" 3 | import BotSetting from "./BotSetting" 4 | 5 | const AdminPage = () => ( 6 | 7 |
16 | 17 | 18 | 19 | 20 | ) 21 | 22 | export default AdminPage 23 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/SearchInput/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useAdminProvider } from "../../../../providers/AdminProvider" 3 | 4 | const SearchInput = () => { 5 | const { filterKey, setFilterKey } = useAdminProvider() 6 | return ( 7 | setFilterKey(e.target.value)} 10 | className="rounded-full 11 | placeholder:text-[gray] 12 | font-poppins 13 | text-[10px] md:text-[16px] 14 | md:w-[250px] md:h-[45px] 15 | w-[180px] h-[30px]" 16 | placeholder="Search By Telegram ID" 17 | /> 18 | ) 19 | } 20 | 21 | export default SearchInput 22 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/SkeletonTableBody.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | import React from "react" 3 | 4 | const SkeletonTableBody = () => ( 5 | 6 | {Array(4) 7 | .fill(null) 8 | .map((_, index) => ( 9 | 10 | 13 | 16 | 17 | ))} 18 | 19 | ) 20 | 21 | export default SkeletonTableBody 22 | -------------------------------------------------------------------------------- /server/bot/rejoin.ts: -------------------------------------------------------------------------------- 1 | import User from "../model/UserModel" 2 | 3 | const rejoin = (bot: any) => { 4 | bot.command("rejoin", async (ctx: any) => { 5 | const userid = ctx.from.id 6 | const username = ctx.from.username 7 | 8 | let user = await User.findOne({ userid }).lean() 9 | 10 | if (user) { 11 | ctx.reply("You have already joined!!!") 12 | return 13 | } 14 | 15 | const newUser = new User({ 16 | username, 17 | userid, 18 | }) 19 | 20 | await newUser.save() 21 | 22 | ctx.reply( 23 | "You have rejoined to weather bot! \nYou can access weather uodates using /weather command.", 24 | ) 25 | }) 26 | } 27 | 28 | export default rejoin 29 | -------------------------------------------------------------------------------- /components/Layout/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ILayout } from "./types" 3 | import SeoHead from "../SeoHead" 4 | import Header from "../Header" 5 | 6 | function BaseLayout({ children }: ILayout) { 7 | return ( 8 |
15 |
16 | 21 |
{children}
22 |
23 | ) 24 | } 25 | 26 | export default BaseLayout 27 | -------------------------------------------------------------------------------- /server/model/UserModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models, Model, Document } from "mongoose" 2 | 3 | interface UserModel extends Document { 4 | username: string 5 | userid: number 6 | time: Date 7 | blocked: Boolean 8 | subscribed: Boolean 9 | } 10 | 11 | const UserSchema = new Schema({ 12 | username: { 13 | type: String, 14 | }, 15 | userid: { 16 | type: Number, 17 | required: true, 18 | }, 19 | time: { 20 | type: Date, 21 | default: Date.now, 22 | }, 23 | blocked: { 24 | type: Boolean, 25 | default: false, 26 | }, 27 | subscribed: { 28 | type: Boolean, 29 | default: false, 30 | }, 31 | }) 32 | 33 | export default (models.User as Model) || model("User", UserSchema) 34 | -------------------------------------------------------------------------------- /server/ApplicationModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common" 2 | import { RenderModule } from "nest-next" 3 | import Next from "next" 4 | import { NODE_ENV } from "../utils/constants/env" 5 | import AppController from "./AppController" 6 | import AdminController from "./controller/AdminController" 7 | import UserController from "./controller/UserController" 8 | import BotController from "./controller/BotController" 9 | 10 | @Module({ 11 | imports: [ 12 | RenderModule.forRootAsync( 13 | Next({ 14 | dev: NODE_ENV !== "production", 15 | conf: { useFilesystemPublicRoutes: false }, 16 | }), 17 | ), 18 | ], 19 | controllers: [AppController, AdminController, UserController, BotController], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /server/bot/subscribe.ts: -------------------------------------------------------------------------------- 1 | import User from "../model/UserModel" 2 | 3 | const subscribe = (bot: any) => { 4 | bot.command("subscribe", async (ctx: any) => { 5 | const userid = ctx.from.id 6 | 7 | let user = await User.findOne({ userid }).lean() 8 | 9 | if (!user) { 10 | ctx.reply("You have not registered!!!") 11 | return 12 | } 13 | 14 | if (user?.subscribed) { 15 | ctx.reply("You have already subscribed to weather updates!") 16 | return 17 | } 18 | 19 | await User.findOneAndUpdate({ userid }, { subscribed: true }).lean() 20 | 21 | ctx.reply( 22 | "You have subscribed to weather updates! \nYou can access weather uodates using /weather command.", 23 | ) 24 | }) 25 | } 26 | 27 | export default subscribe 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | # db account keys 34 | utils/db/serviceAccountKey.json 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | 41 | # ENV 42 | .env 43 | .env.* 44 | .env.local 45 | .env.production 46 | 47 | # Testing 48 | pages/api/temp.ts 49 | -------------------------------------------------------------------------------- /lib/telegramUser.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | export const blockUser = async (userid: number) => { 4 | try { 5 | await axios.post("/api/user/blocked", { userid }) 6 | 7 | return true 8 | } catch (err) { 9 | return { err } 10 | } 11 | } 12 | 13 | export const unblockUser = async (userid: number) => { 14 | try { 15 | await axios.post("/api/user/unblocked", { userid }) 16 | 17 | return true 18 | } catch (err) { 19 | return { err } 20 | } 21 | } 22 | 23 | export const deleteUser = async (userid: number) => { 24 | try { 25 | const config = { 26 | params: { userid }, 27 | } 28 | 29 | await axios.delete("/api/user/deleted", config) 30 | 31 | return true 32 | } catch (err) { 33 | return { err } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/PageButton/PageButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from "react" 2 | 3 | interface ButtonProps { 4 | id: string 5 | children?: ReactNode 6 | className?: string 7 | type?: "button" | "submit" | "reset" | undefined 8 | onClick?: (e: any) => void 9 | disabled?: boolean 10 | hasDoubleAnimation?: boolean 11 | } 12 | 13 | const PageButton: FC = ({ children, className, ...rest }) => ( 14 | 26 | ) 27 | 28 | export default PageButton 29 | -------------------------------------------------------------------------------- /components/SeoHead/SeoHead.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Head from "next/head" 3 | 4 | function SeoHead(props: any) { 5 | const { description, image, title } = props 6 | return ( 7 | 8 | {title} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default SeoHead 25 | -------------------------------------------------------------------------------- /server/model/AdminModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models, Model, Document } from "mongoose" 2 | 3 | interface AdminModel extends Document { 4 | email: string 5 | email_verified: Boolean 6 | family_name: string 7 | given_name: string 8 | locale: string 9 | name: string 10 | picture: string 11 | sub: number 12 | } 13 | 14 | const AdminSchema = new Schema({ 15 | email: { 16 | type: String, 17 | required: [true, "Email is required"], 18 | }, 19 | email_verified: { 20 | type: Boolean, 21 | }, 22 | family_name: { 23 | type: String, 24 | }, 25 | given_name: { 26 | type: String, 27 | }, 28 | locale: { 29 | type: String, 30 | }, 31 | name: { 32 | type: String, 33 | }, 34 | picture: { 35 | type: String, 36 | }, 37 | sub: { 38 | type: Number, 39 | }, 40 | }) 41 | 42 | export default (models.Admin as Model) || model("Admin", AdminSchema) 43 | -------------------------------------------------------------------------------- /shared/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode, useState, useEffect } from "react" 2 | 3 | interface SwitchProps { 4 | id: string 5 | onClick?: () => void 6 | children?: ReactNode 7 | value?: boolean 8 | } 9 | 10 | const Switch: FC = ({ onClick, value }) => { 11 | const [isToggle, setIsToggle] = useState(false) 12 | 13 | useEffect(() => { 14 | setIsToggle(value) 15 | }, [value]) 16 | 17 | return ( 18 |
19 |
23 |
28 |
29 |
30 | ) 31 | } 32 | 33 | export default Switch 34 | -------------------------------------------------------------------------------- /components/MobileMenu/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Link from "next/link" 3 | import Media from "../../shared/Media" 4 | import MenuList from "./MenuList" 5 | 6 | const MobileMenu = () => ( 7 |
11 | 12 |
13 | 20 |

25 | Weather Bot 26 |

27 |
28 | 29 | 30 |
31 | ) 32 | 33 | export default MobileMenu 34 | -------------------------------------------------------------------------------- /shared/Popover/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | 3 | interface IClickOutsideHook { 4 | shouldRegister: boolean 5 | onOutsideClick: () => any 6 | } 7 | 8 | function useClickOutside({ shouldRegister, onOutsideClick }: IClickOutsideHook) { 9 | const ref = useRef(null) 10 | 11 | const handleMouseClick = async (e: MouseEvent) => { 12 | const node = e.target as Node 13 | 14 | if (ref?.current?.contains(node)) return 15 | 16 | await onOutsideClick() 17 | } 18 | 19 | useEffect(() => { 20 | if (!shouldRegister) { 21 | document.removeEventListener("mousedown", handleMouseClick) 22 | return 23 | } 24 | 25 | document.addEventListener("mousedown", handleMouseClick) 26 | 27 | return () => { 28 | document.removeEventListener("mousedown", handleMouseClick) 29 | } 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, [shouldRegister]) 32 | 33 | return { ref } 34 | } 35 | 36 | export default useClickOutside 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weather Bot UI AST 2 | 3 | Screenshot 2023-10-18 at 6 34 08 AM 4 | 5 | ## Getting Started 6 | 7 | First, install packages with 8 | yarn 9 | 10 | ```bash 11 | yarn 12 | ``` 13 | 14 | Next, copy the `.env.local` file to `.env` and fill in: 15 | 16 | - BOT_TOKEN=6576938381:AAG.....VVK_NM 17 | - WEATHER_API_KEY=89e0.....096d5 18 | - MONGO_URI=mongodb://localhost:27017/telebot 19 | - PORT=3000 20 | - NEXT_PUBLIE_GOOGLE_CLIENT_ID=551764290924-trda8.....5lcp.apps.googleusercontent.com 21 | 22 | Finally, run the development server: 23 | 24 | ```bash 25 | yarn dev 26 | ``` 27 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 28 | 29 | Build the production: 30 | 31 | ```bash 32 | yarn build 33 | ``` 34 | 35 | Format code style & syntax 36 | 37 | ```bash 38 | yarn format 39 | ``` 40 | 41 | ## Next.js + Nest.js with Typescript 42 | 🛳️ Built by Ronald Pelham 🛳️ 43 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core" 2 | import { AppModule } from "./ApplicationModule" 3 | import { NestExpressApplication } from "@nestjs/platform-express" 4 | import mongoose from "mongoose" 5 | import { MONGO_URI } from "../utils/constants/env" 6 | import { join } from "path" 7 | import launch from "./bot/WeatherBot" 8 | 9 | const BOT_TOKEN = process.env.BOT_TOKEN || "" 10 | 11 | async function bootstrap() { 12 | const opts = { 13 | bufferCommands: false, 14 | } 15 | 16 | mongoose.set("strictQuery", false) 17 | // Connect to MongoDB database 18 | mongoose 19 | .connect(MONGO_URI, opts) 20 | .then(() => { 21 | console.log("Connected to Database!") 22 | }) 23 | .catch((error) => { 24 | console.log("Error connecting to Database : ", error.message) 25 | }) 26 | 27 | launch(BOT_TOKEN) 28 | 29 | const server = await NestFactory.create(AppModule) 30 | 31 | server.useStaticAssets(join(__dirname, "..", "public")) 32 | 33 | await server.listen(3000) 34 | } 35 | 36 | bootstrap() 37 | -------------------------------------------------------------------------------- /server/bot/start.ts: -------------------------------------------------------------------------------- 1 | import User from "../model/UserModel" 2 | 3 | const start = (bot: any) => { 4 | bot.start(async (ctx: any) => { 5 | const userid = ctx.from.id 6 | const username = ctx.from.username 7 | 8 | let user = await User.findOne({ userid }) 9 | 10 | if (user) { 11 | ctx.reply( 12 | "Welcome to the WEATHER UPDATE bot!\nThis is a publicly available bot made by Henry to get the weather information of a particular city of your choice... \n\n\nTap /help - To Get help with commands\n\nMade with ❤️ by Henry Ziad", 13 | ) 14 | return 15 | } 16 | 17 | user = new User({ 18 | username, 19 | userid, 20 | }) 21 | 22 | try { 23 | await user.save() 24 | ctx.reply( 25 | "Welcome to the WEATHER UPDATE bot!\nThis is a publicly available bot made by Henry to get the weather information of a particular city of your choice... \n\n\nTap /help - To Get help with commands\n\nMade with ❤️ by Henry Ziad", 26 | ) 27 | } catch (err: any) { 28 | throw new Error(err) 29 | } 30 | }) 31 | } 32 | 33 | export default start 34 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css" 2 | import React, { useEffect } from "react" 3 | import type { AppProps } from "next/app" 4 | import { GoogleOAuthProvider } from "@react-oauth/google" 5 | import AOS from "aos" 6 | import { UserProvider } from "../providers/UserProvider" 7 | import { ThemeProvider } from "../providers/ThemeProvider" 8 | import "aos/dist/aos.css" 9 | import { AdminProvider } from "../providers/AdminProvider" 10 | 11 | function MyApp({ Component, pageProps }: AppProps) { 12 | useEffect(() => { 13 | AOS.init({ 14 | duration: 800, 15 | once: false, 16 | }) 17 | }, []) 18 | 19 | return ( 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | export default MyApp 37 | -------------------------------------------------------------------------------- /server/bot/WeatherBot.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf } from "telegraf" 2 | import start from "./start" 3 | import help from "./help" 4 | import weather from "./weather" 5 | import subscribe from "./subscribe" 6 | import unsubscribe from "./unsubscribe" 7 | import rejoin from "./rejoin" 8 | 9 | let WeatherBot: any 10 | 11 | const launch = (botKey: string) => { 12 | try { 13 | WeatherBot = new Telegraf(botKey) 14 | 15 | start(WeatherBot) 16 | 17 | help(WeatherBot) 18 | 19 | subscribe(WeatherBot) 20 | 21 | unsubscribe(WeatherBot) 22 | 23 | weather(WeatherBot) 24 | 25 | rejoin(WeatherBot) 26 | 27 | WeatherBot.launch().then(() => { 28 | console.log(`Bot-${botKey} is running`) 29 | }) 30 | WeatherBot.catch((error: any) => { 31 | console.log("Bot Error:", error) 32 | }) 33 | } catch (err) { 34 | console.log("Bot Error: ", err) 35 | } 36 | } 37 | 38 | export const stop = (botKey: string) => { 39 | try { 40 | WeatherBot.stop("Stopped by admin") 41 | console.log(`Bot-${botKey} is stopped`) 42 | } catch (err) { 43 | console.log(`Bot-${botKey} is not running`) 44 | } 45 | } 46 | 47 | export default launch 48 | -------------------------------------------------------------------------------- /providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { useLocalStorage } from "usehooks-ts" 4 | 5 | interface Props { 6 | children: React.ReactNode 7 | } 8 | 9 | const ThemeContext = React.createContext(null) 10 | 11 | export const ThemeProvider: React.FC = ({ children }) => { 12 | const [themeMode, setThemeMode] = useLocalStorage("theme", "light") 13 | 14 | const toggleMode = () => { 15 | const html = document.querySelector("html")! 16 | if (themeMode == "light") { 17 | html.classList.remove("dark") 18 | } else { 19 | html.classList.add("dark") 20 | } 21 | } 22 | 23 | const provider = { 24 | themeMode, 25 | onChangeThemeConfig: (mode?: string) => { 26 | if (mode === undefined) { 27 | setThemeMode(themeMode == "light" ? "dark" : "light") 28 | return 29 | } 30 | setThemeMode(mode) 31 | }, 32 | } 33 | 34 | React.useEffect(() => { 35 | toggleMode() 36 | }, [themeMode]) 37 | 38 | return {children} 39 | } 40 | 41 | export const useTheme = () => React.useContext(ThemeContext) 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "strict": true, 7 | "pretty": true, 8 | "noImplicitAny": true, 9 | "alwaysStrict": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "outDir": ".next", 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "lib": [ 21 | "es2017", 22 | "dom" 23 | ], 24 | "baseUrl": ".", 25 | "typeRoots": [ 26 | "node_modules/@types", 27 | "./typings" 28 | ], 29 | "allowJs": true, 30 | "forceConsistentCasingInFileNames": true, 31 | "strictPropertyInitialization": false, 32 | "noEmit": true, 33 | "resolveJsonModule": true, 34 | "isolatedModules": true, 35 | "incremental": true 36 | }, 37 | "include": [ 38 | "./pages/**/*", 39 | "./server/**/*", 40 | "./ui/**/*", 41 | "globals.d.ts" 42 | ], 43 | "exclude": [ 44 | "node_modules" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /shared/Media.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { CSSProperties, DetailedHTMLProps, VideoHTMLAttributes } from "react" 3 | import React from 'react' 4 | 5 | interface IMedia { 6 | type: "video" | "image" 7 | link?: string 8 | posterLink?: string 9 | containerStyle?: CSSProperties 10 | containerClasses?: string 11 | className?: string 12 | videoProps?: DetailedHTMLProps, HTMLVideoElement> 13 | blurLink?: string 14 | alt?: string 15 | } 16 | 17 | function Media({ type, link, containerClasses, containerStyle, blurLink, alt }: IMedia) { 18 | return ( 19 |
20 | {type === "image" && link && ( 21 | 33 | )} 34 |
35 | ) 36 | } 37 | 38 | export default Media 39 | -------------------------------------------------------------------------------- /shared/Form.tsx: -------------------------------------------------------------------------------- 1 | import { joiResolver } from "@hookform/resolvers/joi" 2 | import React, { FormEvent, ReactNode } from "react" 3 | import { FormProvider, SubmitErrorHandler, useForm } from "react-hook-form" 4 | 5 | interface IForm { 6 | id?: string 7 | onSubmit: (values: any) => any 8 | onError?: SubmitErrorHandler 9 | children: ReactNode 10 | className?: string 11 | initialValues?: Object 12 | validationSchema: any 13 | } 14 | 15 | function Form({ 16 | id, 17 | onSubmit, 18 | validationSchema, 19 | children, 20 | onError, 21 | className, 22 | initialValues, 23 | }: IForm) { 24 | const formMethods = useForm({ 25 | resolver: joiResolver(validationSchema), 26 | defaultValues: initialValues, 27 | }) 28 | 29 | const handleFormSubmit = async (event: FormEvent) => { 30 | await formMethods.handleSubmit(onSubmit, onError)(event) 31 | } 32 | 33 | return ( 34 | 35 |
36 | {children} 37 | 38 |
39 | ) 40 | } 41 | 42 | Form.defaultProps = { 43 | onSubmit: () => {}, 44 | children: "", 45 | className: "", 46 | initialValues: {}, 47 | } 48 | 49 | export default Form 50 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /** @type {import('tailwindcss').Config} */ 3 | const defaultTheme = require('tailwindcss/defaultTheme') 4 | 5 | module.exports = { 6 | content: [ 7 | "./pages/**/*.{js,ts,jsx,tsx}", 8 | "./components/**/*.{js,ts,jsx,tsx}", 9 | "./shared/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | }, 15 | extend: { 16 | fontFamily: { 17 | poppins: ["Poppins", "sans-serif"], 18 | poppins_light: ["Poppins Light", "sans-serif"], 19 | poppins_medium: ["Poppins Medium", "sans-serif"], 20 | poppins_semibold: ["Poppins SemiBold", "sans-serif"], 21 | poppins_bold: ["Poppins Bold", "sans-serif"], 22 | }, 23 | screens: { 24 | ios : '320px', 25 | samsungS8: "360px", 26 | xs: '390px', 27 | sm: "640px", 28 | md: "768px", 29 | lg: "1024px", 30 | xl: "1280px", 31 | "2xl":'1448px', 32 | "3xl": "1920px", 33 | }, 34 | }, 35 | }, 36 | variants: { 37 | extend: { 38 | display: ["dark"], 39 | }, 40 | }, 41 | darkMode: ["class"], 42 | plugins: [ 43 | require("@tailwindcss/forms"), 44 | require("tailwind-scrollbar")({ nocompatible: true }), 45 | // ... 46 | ], 47 | } 48 | -------------------------------------------------------------------------------- /components/Pages/HomePage/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../../Layout" 2 | import Button from "../../../shared/Button" 3 | 4 | const HomePage = () => ( 5 | 6 |
14 |

21 | Weather Telegram Bot 22 |

23 | 36 |
37 |
38 | ) 39 | 40 | export default HomePage 41 | -------------------------------------------------------------------------------- /shared/Button/usePulseEffect.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-param-reassign */ 2 | import { useEffect } from "react" 3 | import styles from "./Button.module.css" 4 | 5 | interface Props { 6 | ref?: any 7 | pulseRef?: any 8 | pulseColor?: string 9 | } 10 | 11 | const usePulseEffect = ({ ref, pulseRef, pulseColor }: Props) => { 12 | useEffect(() => { 13 | const handleMouseClick = async (event: any) => { 14 | if(pulseRef?.current && ref?.current) { 15 | pulseRef.current.classList.remove(styles.pulse__effect) 16 | const posX = event?.clientX 17 | const posY = event?.clientY 18 | 19 | const topY = ref.current.getBoundingClientRect().top 20 | const leftX = ref.current.getBoundingClientRect().left 21 | 22 | const offsetX = posX - leftX 23 | const offsetY = posY - topY 24 | 25 | pulseRef.current.style.left = `${offsetX}px` 26 | pulseRef.current.style.top = `${offsetY}px` 27 | pulseRef.current.style.backgroundColor = pulseColor || "white" 28 | 29 | pulseRef.current.classList.add(styles.pulse__effect) 30 | } 31 | } 32 | 33 | if (ref?.current && pulseRef?.current) { 34 | ref.current.removeEventListener("click", handleMouseClick) 35 | ref.current.addEventListener("click", handleMouseClick) 36 | } 37 | }, [ref, pulseRef, pulseColor]) 38 | } 39 | 40 | export default usePulseEffect 41 | -------------------------------------------------------------------------------- /providers/UserProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, createContext, useEffect } from "react" 2 | import { useLocalStorage } from "usehooks-ts" 3 | import getUserData from "../lib/getUserData" 4 | import { useRouter } from "next/router" 5 | 6 | interface Props { 7 | children: React.ReactNode 8 | } 9 | 10 | const UserContext = createContext(null) 11 | 12 | export const UserProvider: React.FC = ({ children }) => { 13 | const [credentialToken, setCredentialToken] = useLocalStorage("credentialToken", "") 14 | const [userData, setUserData] = useLocalStorage("userData", "") 15 | const router = useRouter() 16 | 17 | useEffect(() => { 18 | if (!credentialToken && !router.pathname.includes("/home")) router.push("/") 19 | if (credentialToken && router.pathname.includes("/home")) router.push("/admin") 20 | }, [credentialToken]) 21 | 22 | useEffect(() => { 23 | if (credentialToken && !userData) { 24 | const init = async () => { 25 | const data = await getUserData(credentialToken) 26 | setUserData(data) 27 | } 28 | init() 29 | } 30 | }, [credentialToken]) 31 | 32 | const value = useMemo( 33 | () => ({ credentialToken, setCredentialToken, userData, setUserData }), 34 | [credentialToken, setCredentialToken, userData, setUserData], 35 | ) 36 | 37 | return {children} 38 | } 39 | 40 | export const useUserCredential = () => React.useContext(UserContext) 41 | -------------------------------------------------------------------------------- /components/GoogleLoginButton/GoogleLoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useGoogleLogin, googleLogout } from "@react-oauth/google" 3 | import { useUserCredential } from "providers/UserProvider" 4 | import Button from "../../shared/Button" 5 | import addAdminData from "../../lib/addAdminData" 6 | import getUserData from "../../lib/getUserData" 7 | 8 | const GoogleLoginButton = () => { 9 | const { setCredentialToken, credentialToken, setUserData } = useUserCredential() 10 | 11 | const onSuccess = async (tokenResponse: any) => { 12 | setCredentialToken(tokenResponse) 13 | const data = await getUserData(tokenResponse) 14 | await addAdminData(data) 15 | } 16 | 17 | const login = useGoogleLogin({ 18 | onSuccess, 19 | }) 20 | 21 | const logout = () => { 22 | googleLogout() 23 | setCredentialToken("") 24 | setUserData("") 25 | } 26 | 27 | return ( 28 | 42 | ) 43 | } 44 | 45 | export default GoogleLoginButton 46 | -------------------------------------------------------------------------------- /server/controller/AdminController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Body, Render } from "@nestjs/common" 2 | import AdminModel from "../model/AdminModel" 3 | import AdminDTO from "../../DTO/admin.dto" 4 | 5 | @Controller() 6 | class AdminController { 7 | // eslint-disable-next-line no-useless-constructor, no-empty-function 8 | constructor() {} 9 | 10 | @Render("admin") 11 | @Get("/admin") 12 | public index() {} 13 | 14 | @Get("/api/admin/all") 15 | public async getAdminAll() { 16 | try { 17 | const admins = await AdminModel.find({}).lean() 18 | return { 19 | data: admins as any, 20 | } 21 | } catch (err: any) { 22 | throw new Error(err) 23 | } 24 | } 25 | 26 | @Post("/api/admin/getOne") 27 | public async getAdminByUserName(@Body() body: { email: string }) { 28 | const { email } = body 29 | 30 | try { 31 | const admin = await AdminModel.findOne({ email }).lean() 32 | 33 | return admin as any 34 | } catch (err: any) { 35 | throw new Error(err) 36 | } 37 | } 38 | 39 | @Post("/api/admin/add") 40 | public async addNewAdmin(@Body() body: AdminDTO) { 41 | try { 42 | const userData = await AdminModel.findOne({ email: body.email }) 43 | 44 | if (userData) return userData 45 | 46 | const data = await AdminModel.create({ 47 | ...body, 48 | }) 49 | 50 | return data 51 | } catch (err: any) { 52 | throw new Error(err) 53 | } 54 | } 55 | } 56 | 57 | export default AdminController 58 | -------------------------------------------------------------------------------- /server/controller/UserController.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller, Post, Body, Delete, Query } from "@nestjs/common" 2 | import User from "../model/UserModel" 3 | 4 | @Controller() 5 | class UserController { 6 | // eslint-disable-next-line no-useless-constructor, no-empty-function 7 | constructor() {} 8 | 9 | @Get("/api/user/all") 10 | public async getAllUsers() { 11 | try { 12 | const users = await User.find({}).lean() 13 | 14 | return users as any 15 | } catch (err: any) { 16 | throw new Error(err) 17 | } 18 | } 19 | 20 | @Post("/api/user/blocked") 21 | public async userBlocked(@Body() body: { userid: Number }) { 22 | const { userid } = body 23 | 24 | try { 25 | await User.findOneAndUpdate({ userid }, { blocked: true }).lean() 26 | 27 | return { message: "blocked" } 28 | } catch (err: any) { 29 | throw new Error(err) 30 | } 31 | } 32 | 33 | @Post("/api/user/unblocked") 34 | public async userUnBlocked(@Body() body: { userid: Number }) { 35 | const { userid } = body 36 | 37 | try { 38 | await User.findOneAndUpdate({ userid }, { blocked: false }).lean() 39 | 40 | return { message: "unblocked" } 41 | } catch (err: any) { 42 | throw new Error(err) 43 | } 44 | } 45 | 46 | @Delete("/api/user/deleted") 47 | public async deleteUser(@Query("userid") userid: number) { 48 | try { 49 | await User.findOneAndRemove({ userid }).lean() 50 | 51 | return { message: "deleted" } 52 | } catch (err: any) { 53 | throw new Error(err) 54 | } 55 | } 56 | } 57 | 58 | export default UserController 59 | -------------------------------------------------------------------------------- /shared/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, LegacyRef, ReactNode, useRef } from "react" 2 | import usePulseEffect from "./usePulseEffect" 3 | 4 | interface ButtonProps { 5 | id: string 6 | refP?: Element 7 | children?: ReactNode 8 | className?: string 9 | type?: "button" | "submit" | "reset" | undefined 10 | pulseColor?: string 11 | onClick?: (e: any) => void 12 | disabled?: boolean 13 | } 14 | 15 | const Button: FC = ({ 16 | id, 17 | refP, 18 | children, 19 | pulseColor, 20 | className, 21 | onClick, 22 | ...rest 23 | }) => { 24 | const elementRef = useRef() as any 25 | const ref = useRef() as any 26 | const pulseRef = useRef() as any 27 | 28 | usePulseEffect({ 29 | ref, 30 | pulseRef, 31 | pulseColor, 32 | }) 33 | 34 | return ( 35 | 66 | ) 67 | } 68 | 69 | export default Button 70 | -------------------------------------------------------------------------------- /shared/Popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { useState } from "react" 3 | import useClickOutside from "./useClickOutside" 4 | 5 | interface IPopoverFucChild { 6 | toggleModal?: () => void 7 | openModal?: boolean 8 | } 9 | 10 | interface PopoverProps { 11 | id: string 12 | children?: [(props: IPopoverFucChild) => any, (props: IPopoverFucChild) => any] 13 | className?: string 14 | open?: boolean 15 | } 16 | 17 | export default function Popover({ id, children, className }: PopoverProps) { 18 | const [openModal, setOpenModal] = useState(false) 19 | 20 | const toggleModal = (e: any) => { 21 | e.stopPropagation() 22 | if (openModal) return setOpenModal(false) 23 | setOpenModal((prev) => !prev) 24 | } 25 | 26 | const { ref: modalRef } = useClickOutside({ 27 | shouldRegister: openModal, 28 | onOutsideClick: () => setOpenModal(false), 29 | }) 30 | 31 | return ( 32 |
33 |
34 | {children && typeof children[0] !== "function" && children[0]} 35 | {children && 36 | typeof children[0] === "function" && 37 | (children[0] as any)({ 38 | openModal, 39 | })} 40 |
41 | {openModal && ( 42 |
43 | {children && typeof children[1] !== "function" && children[1]} 44 | {children && 45 | typeof children[1] === "function" && 46 | (children[1] as any)({ 47 | toggleModal, 48 | })} 49 |
50 | )} 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /server/controller/BotController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, Get } from "@nestjs/common" 2 | import BotModel from "../model/BotModel" 3 | import launch, { stop } from "../bot/WeatherBot" 4 | 5 | @Controller() 6 | class BotController { 7 | // eslint-disable-next-line no-useless-constructor, no-empty-function 8 | constructor() {} 9 | 10 | @Post("/api/bot/update") 11 | public async updateBotKey(@Body() body: { key: string }) { 12 | const { key } = body 13 | 14 | try { 15 | const bot = await BotModel.findOneAndUpdate({ key }).lean() 16 | 17 | return bot as any 18 | } catch (err: any) { 19 | throw new Error(err) 20 | } 21 | } 22 | 23 | @Post("/api/bot/relanuch") 24 | public async relaunch(@Body() body: { newKey: string; oldKey: string; newHandle: string }) { 25 | const { newKey, oldKey, newHandle } = body 26 | try { 27 | stop(oldKey) 28 | 29 | const updatedBot = await BotModel.findOneAndUpdate( 30 | { key: oldKey }, 31 | { key: newKey, handle: newHandle }, 32 | ).lean() 33 | 34 | if (!updatedBot) { 35 | const newBot = new BotModel({ 36 | key: newKey, 37 | handle: newHandle, 38 | }) 39 | 40 | await newBot.save() 41 | } 42 | 43 | launch(newKey) 44 | } catch (err: any) { 45 | console.log(err) 46 | throw new Error(err) 47 | } 48 | } 49 | 50 | @Get("/api/bot/getKey") 51 | public async getKey() { 52 | try { 53 | const bots = await BotModel.find({}).lean() 54 | 55 | if (bots.length) return bots[0] 56 | 57 | return { key: "" } 58 | } catch (err: any) { 59 | throw new Error(err) 60 | } 61 | } 62 | } 63 | 64 | export default BotController 65 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Poppins"; 7 | src: url("/fonts/Poppins/Poppins-Regular.ttf") format("truetype"); 8 | } 9 | @font-face { 10 | font-family: "Poppins Light"; 11 | src: url("/fonts/Poppins/Poppins-Light.ttf") format("truetype"); 12 | } 13 | @font-face { 14 | font-family: "Poppins Medium"; 15 | src: url("/fonts/Poppins/Poppins-Medium.ttf") format("truetype"); 16 | } 17 | @font-face { 18 | font-family: "Poppins SemiBold"; 19 | src: url("/fonts/Poppins/Poppins-SemiBold.ttf") format("truetype"); 20 | } 21 | @font-face { 22 | font-family: "Poppins Bold"; 23 | src: url("/fonts/Poppins/Poppins-Bold.ttf") format("truetype"); 24 | } 25 | 26 | @layer utilities { 27 | .no-scrollbar::-webkit-scrollbar { 28 | display: none; 29 | } 30 | 31 | .no-scrollbar { 32 | -ms-overflow-style: none; 33 | scrollbar-width: none; 34 | } 35 | } 36 | html, 37 | body { 38 | background-color: white; 39 | padding: 0; 40 | margin: 0; 41 | font-family: 42 | -apple-system, 43 | BlinkMacSystemFont, 44 | Segoe UI, 45 | Roboto, 46 | Oxygen, 47 | Ubuntu, 48 | Cantarell, 49 | Fira Sans, 50 | Droid Sans, 51 | Helvetica Neue, 52 | sans-serif; 53 | } 54 | 55 | p { 56 | margin: 0; 57 | padding: 0; 58 | } 59 | body::-webkit-scrollbar { 60 | display: none; 61 | } 62 | 63 | ::-webkit-scrollbar { 64 | width: 13px; 65 | } 66 | 67 | ::-webkit-scrollbar-track { 68 | background: black; 69 | border-radius: 10px; 70 | border: 1px solid white; 71 | } 72 | 73 | ::-webkit-scrollbar-thumb { 74 | border-radius: 10px; 75 | border: 3px solid rgba(0, 0, 0, 0); 76 | background-clip: padding-box; 77 | background-color: white; 78 | } 79 | -------------------------------------------------------------------------------- /components/MobileMenu/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import dynamic from "next/dynamic" 3 | import Media from "../../shared/Media" 4 | import Popover from "../../shared/Popover" 5 | import Switch from "../../shared/Switch" 6 | import { useTheme } from "../../providers/ThemeProvider" 7 | 8 | const GoogleLoginButton = dynamic(() => import("../GoogleLoginButton"), { 9 | ssr: false, 10 | }) 11 | 12 | const MenuList = () => { 13 | const { onChangeThemeConfig, themeMode } = useTheme() 14 | 15 | const onToggle = () => { 16 | onChangeThemeConfig() 17 | } 18 | 19 | return ( 20 | 21 | {() => ( 22 | 28 | )} 29 | {() => ( 30 |
40 | 47 | 48 |
49 |

Light

50 | 51 |

Dark

52 |
53 |
54 | )} 55 |
56 | ) 57 | } 58 | 59 | export default MenuList 60 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import SkeletonTableBody from "../SkeletonTableBody" 3 | import TableRow from "../TableRow" 4 | import TableHead from "../TableHead" 5 | import { TelegramUser } from "../types" 6 | import SearchInput from "../SearchInput" 7 | import Pagination from "../Pagination" 8 | import { useAdminProvider } from "../../../../providers/AdminProvider" 9 | 10 | const Table = () => { 11 | const { paginatedUser } = useAdminProvider() 12 | 13 | return ( 14 | <> 15 |
16 | 17 |
18 |
11 |
12 |
14 |
15 |
22 | 23 | 24 | User ID 25 | User Name 26 | Join At 27 | Action 28 | 29 | 30 | {paginatedUser ? ( 31 | 32 | {paginatedUser.length ? ( 33 | paginatedUser.map((user: TelegramUser) => ) 34 | ) : ( 35 | 36 | 39 | 40 | )} 41 | 42 | ) : ( 43 | 44 | )} 45 |
37 | No Telegram Users 38 |
46 |
47 | 48 |
49 | 50 | ) 51 | } 52 | 53 | export default Table 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the main branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | - test 13 | 14 | jobs: 15 | run-linters: 16 | name: Run linters 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out Git repository 20 | uses: actions/checkout@v3.1.0 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v3.5.1 24 | with: 25 | node-version: 16 26 | registry-url: "https://npm.pkg.github.com" 27 | 28 | - name: Get yarn cache directory path 29 | id: yarn-cache-dir-path 30 | run: echo "::set-output name=dir::$(yarn cache dir)" 31 | 32 | - name: Cache yarn cache 33 | uses: actions/cache@v3.0.11 34 | id: cache-yarn-cache 35 | with: 36 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 37 | key: ${{ runner.os }}-yarn-dev-${{ hashFiles('**/yarn.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-yarn-dev- 40 | 41 | - name: Cache node_modules 42 | id: cache-node-modules 43 | uses: actions/cache@v3.0.11 44 | with: 45 | path: node_modules 46 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-dev-${{ hashFiles('**/yarn.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-dev- 49 | 50 | - name: Install Dependencies 51 | run: yarn install --frozen-lockfile --prefer-offline 52 | 53 | - name: Fix Formatting 54 | run: yarn format 55 | 56 | - name: Check linting 57 | run: yarn lint 58 | 59 | - name: Commit changes 60 | uses: stefanzweifel/git-auto-commit-action@v4 61 | with: 62 | commit_message: Apply formatting changes 63 | branch: ${{ github.head_ref }} 64 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the main branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | - test 13 | 14 | jobs: 15 | run-linters: 16 | name: Run linters 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out Git repository 20 | uses: actions/checkout@v3.1.0 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v3.5.1 24 | with: 25 | node-version: 16 26 | registry-url: "https://npm.pkg.github.com" 27 | 28 | - name: Get yarn cache directory path 29 | id: yarn-cache-dir-path 30 | run: echo "::set-output name=dir::$(yarn cache dir)" 31 | 32 | - name: Cache yarn cache 33 | uses: actions/cache@v3.0.11 34 | id: cache-yarn-cache 35 | with: 36 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 37 | key: ${{ runner.os }}-yarn-dev-${{ hashFiles('**/yarn.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-yarn-dev- 40 | 41 | - name: Cache node_modules 42 | id: cache-node-modules 43 | uses: actions/cache@v3.0.11 44 | with: 45 | path: node_modules 46 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-dev-${{ hashFiles('**/yarn.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-dev- 49 | 50 | - name: Install Dependencies 51 | run: yarn install --frozen-lockfile --prefer-offline 52 | 53 | - name: Fix Formatting 54 | run: yarn format 55 | 56 | - name: Check linting 57 | run: yarn lint 58 | 59 | - name: Commit changes 60 | uses: stefanzweifel/git-auto-commit-action@v4 61 | with: 62 | commit_message: Apply formatting changes 63 | branch: ${{ github.head_ref }} 64 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": ["./tsconfig.json"] 6 | }, 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 11 | } 12 | } 13 | }, 14 | "rules": { 15 | "prettier/prettier": [ 16 | "warn", 17 | {}, 18 | { 19 | "usePrettierrc": true 20 | } 21 | ], 22 | "import/prefer-default-export": "off", 23 | "import/no-extraneous-dependencies": "off", 24 | "import/no-unresolved": "off", 25 | "import/extensions": "off", 26 | "no-use-before-define": "off", 27 | "semi": "off", 28 | "no-unused-vars": "off", 29 | "no-shadow": "off", 30 | "class-methods-use-this": "off", 31 | "@typescript-eslint/no-shadow": ["error"], 32 | "@typescript-eslint/semi": "off", 33 | "@typescript-eslint/no-use-before-define": ["error"], 34 | "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], 35 | "react/jsx-props-no-spreading": "off", 36 | "react/react-in-jsx-scope": "off", 37 | "react/require-default-props": "off", 38 | "react/prop-types": "off", 39 | "react/jsx-filename-extension": "off", 40 | "jsx-a11y/label-has-associated-control": [ 41 | 2, 42 | { 43 | "labelComponents": ["CustomInputLabel"], 44 | "labelAttributes": ["label"], 45 | "controlComponents": ["CustomInput"], 46 | "depth": 3 47 | } 48 | ], 49 | "react/function-component-definition": [ 50 | "error", 51 | { 52 | "namedComponents": ["function-declaration", "arrow-function"], 53 | "unnamedComponents": "arrow-function" 54 | } 55 | ] 56 | }, 57 | "plugins": ["@typescript-eslint", "prettier", "react"], 58 | "extends": ["airbnb", "next", "prettier", "plugin:@typescript-eslint/eslint-recommended"], 59 | "globals": { 60 | "Cypress": true, 61 | "JSX": true, 62 | "cy": true, 63 | "document": true, 64 | "navigator": true, 65 | "window": true, 66 | "Rollbar": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /shared/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from "./resolver" 2 | 3 | export type IconsType = keyof typeof Icons 4 | 5 | interface IIcon { 6 | name: IconsType 7 | color?: string 8 | variant: "primary" | "secondary" 9 | size: number 10 | onClick: () => any 11 | raw: boolean 12 | noHighlights: boolean 13 | disabled: boolean 14 | className?: string 15 | } 16 | 17 | const styles = { 18 | base: "w-fit h-fit flex items-center justify-center rounded shadow-sm", 19 | sizes: { 20 | mini: "p-[2px]", 21 | small: "p-1", 22 | medium: "p-3", 23 | large: "p-4", 24 | }, 25 | variants: { 26 | primary: { 27 | color: "text-gray-500 bg-white", 28 | highlight: "duration-75 hover:bg-[rgba(0,0,0,0.01)]", 29 | }, 30 | secondary: { 31 | color: "text-here-purple-900 bg-here-purple-50", 32 | highlight: "duration-75 hover:opacity-75", 33 | }, 34 | }, 35 | states: { 36 | disabled: "cursor-not-allowed pointer-events-none", 37 | }, 38 | } 39 | 40 | function Icon({ 41 | name, 42 | size, 43 | onClick, 44 | raw, 45 | color, 46 | variant, 47 | noHighlights, 48 | disabled, 49 | className, 50 | }: IIcon) { 51 | const IconSVG = Icons[name] 52 | 53 | return raw ? ( 54 | 55 | ) : ( 56 | 79 | ) 80 | } 81 | 82 | const defaultProps: Partial = { 83 | size: 25, 84 | onClick: () => undefined, 85 | raw: false, 86 | noHighlights: false, 87 | variant: "primary", 88 | disabled: false, 89 | } 90 | 91 | Icon.defaultProps = defaultProps 92 | 93 | export default Icon 94 | -------------------------------------------------------------------------------- /shared/Input.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler, useEffect } from "react" 2 | import { useFormContext } from "react-hook-form" 3 | import React from 'react' 4 | 5 | interface IInput { 6 | id?: string 7 | name?: string 8 | value?: any 9 | className?: string 10 | onChange?: ChangeEventHandler 11 | placeholder?: string 12 | hookToForm: boolean 13 | type: "text" | "password" | "url" | "number" 14 | clasNameError?: string 15 | disabled?: boolean 16 | } 17 | 18 | function Input({ 19 | id, 20 | name, 21 | value, 22 | type = "text", 23 | placeholder, 24 | hookToForm, 25 | onChange, 26 | className, 27 | clasNameError, 28 | disabled, 29 | }: IInput) { 30 | const formContext = useFormContext() 31 | const isFullyHooked = name && hookToForm && formContext 32 | 33 | const fieldError = isFullyHooked && formContext?.formState?.errors?.[name] 34 | 35 | useEffect(() => { 36 | if (name && hookToForm) { 37 | formContext.setValue(name, value) 38 | } 39 | }, [value, name, formContext, hookToForm]) 40 | 41 | return ( 42 | 75 | ) 76 | } 77 | 78 | Input.defaultProps = { 79 | hookToForm: false, 80 | type: "text", 81 | } 82 | 83 | export default Input 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestnext8", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon server/main.ts", 8 | "build:clean": "rimraf .next", 9 | "build:ui": "npx next build", 10 | "build:server": "npx tsc --project tsconfig.server.json && npx babel .next/production-server -d .next/production-server --extensions \".js\"", 11 | "build": "yarn build:clean && yarn build:ui && yarn build:server", 12 | "start": "cross-env NODE_ENV=production node .next/production-server/server/main.js", 13 | "prettier:fix": "npx prettier --write components pages providers server hooks", 14 | "format": "yarn run prettier:fix && yarn run lint:fix", 15 | "lint:fix": "next lint --fix", 16 | "lint": "next lint --max-warnings 0" 17 | }, 18 | "dependencies": { 19 | "@heroicons/react": "^2.0.18", 20 | "@hookform/resolvers": "^3.3.2", 21 | "@nestjs/common": "^8.4.4", 22 | "@nestjs/core": "^8.4.4", 23 | "@nestjs/platform-express": "^8.4.4", 24 | "@react-oauth/google": "^0.11.1", 25 | "@tailwindcss/forms": "^0.5.6", 26 | "@types/react-table": "^7.7.16", 27 | "aos": "^2.3.4", 28 | "axios": "^1.5.1", 29 | "class-validator": "^0.14.0", 30 | "cross-env": "^7.0.3", 31 | "eslint-config-airbnb": "^19.0.4", 32 | "mongoose": "^7.6.3", 33 | "nest-next": "^9.6.0", 34 | "next": "12.1.5", 35 | "react": "18.0.0", 36 | "react-dom": "18.0.0", 37 | "react-hook-form": "^7.47.0", 38 | "react-icons": "^4.11.0", 39 | "react-table": "^7.8.0", 40 | "reflect-metadata": "^0.1.13", 41 | "rxjs": "^7.5.5", 42 | "sweetalert": "^2.1.2", 43 | "tailwind-scrollbar": "^3.0.5", 44 | "telegraf": "^4.14.0", 45 | "usehooks-ts": "^2.9.1" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.17.6", 49 | "@babel/core": "^7.17.9", 50 | "@types/aos": "^3.0.5", 51 | "@types/node": "^17.0.23", 52 | "@types/react": "^18.0.4", 53 | "@typescript-eslint/eslint-plugin": "^5.19.0", 54 | "@typescript-eslint/parser": "^5.19.0", 55 | "autoprefixer": "^10.4.16", 56 | "eslint": "^8.13.0", 57 | "eslint-config-next": "^13.5.5", 58 | "eslint-config-prettier": "^8.5.0", 59 | "eslint-plugin-prettier": "^4.0.0", 60 | "joi": "^17.11.0", 61 | "nodemon": "^2.0.15", 62 | "postcss": "^8.4.31", 63 | "prettier": "^2.6.2", 64 | "rimraf": "^3.0.0", 65 | "tailwindcss": "^3.3.3", 66 | "ts-node": "^10.7.0", 67 | "typescript": "^4.6.3" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/bot/weather.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import User from "../model/UserModel" 3 | 4 | const WeatherAPIKey = process.env.WEATHER_API_KEY 5 | let isListening = false 6 | 7 | const weather = (bot: any) => { 8 | bot.command("weather", async (ctx: any) => { 9 | isListening = true 10 | 11 | const user = await User.findOne({ userid: ctx.from.id }) 12 | 13 | if (!user) { 14 | ctx.reply("You are not registered!!!!") 15 | return 16 | } 17 | 18 | if (user?.blocked) { 19 | ctx.reply( 20 | "You have been blocked from using this bot!\nContact the bot owner to unblock you.\n\nTelegram ID: @fullstacktech", 21 | ) 22 | return 23 | } 24 | 25 | if (!user?.subscribed) { 26 | ctx.reply( 27 | "You have not subscribed to get Weather Updates!\nUse /subscribe to subscribe to the bot.", 28 | ) 29 | return 30 | } 31 | 32 | ctx.reply("Enter the name of the city to get weather data: ") 33 | 34 | bot.hears(/.*/, async (ctx: any) => { 35 | if (!isListening) { 36 | ctx.reply("No command Specified...") 37 | return 38 | } 39 | 40 | const messageText = ctx.message.text 41 | const city = messageText 42 | 43 | const apiUrl = `https://api.openweathermap.org/data/2.5/forecast?APPID=${WeatherAPIKey}&q=${city}` 44 | 45 | try { 46 | const response = await axios.get(apiUrl) 47 | 48 | const weatherData = response.data 49 | 50 | const cityName = weatherData.city.name 51 | const country = weatherData.city.country 52 | const date_txt = weatherData.list[0].dt_txt 53 | const date = date_txt.split(" ")[0] 54 | const temperature = weatherData.list[0].main.temp 55 | const condition = weatherData.list[0].weather[0].description 56 | const windSpeed = weatherData.list[0].wind.speed 57 | const coord = weatherData.city.coord 58 | const population = weatherData.city.population 59 | const time = new Date().toLocaleTimeString() 60 | 61 | ctx.reply( 62 | `Current weather in ${cityName}, ${country} :- \n\nTemp: ${temperature}K, ${condition} \nDate: ${date}, Time: ${time}\nWind Speed: ${windSpeed}m/s\nPopulation: ${population}\nCoordinates: ${coord.lon}, ${coord.lat}`, 63 | ) 64 | 65 | isListening = false 66 | } catch (error: any) { 67 | console.log("Error fetching weather data:", error.message) 68 | ctx.reply("City not found!\nPlease enter a valid city name...") 69 | return 70 | } 71 | }) 72 | }) 73 | } 74 | 75 | export default weather 76 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/TableRow/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Button from "shared/Button" 3 | import { useAdminProvider } from "providers/AdminProvider" 4 | import swal from "sweetalert" 5 | import { TelegramUser } from "../types" 6 | import { blockUser, unblockUser, deleteUser } from "../../../../lib/telegramUser" 7 | 8 | const TableRow = ({ user }: { user: TelegramUser }) => { 9 | const { fetchTelegramUsers } = useAdminProvider() 10 | 11 | const updateBlockData = async () => { 12 | if (user.blocked) await unblockUser(user.userid) 13 | else await blockUser(user.userid) 14 | 15 | fetchTelegramUsers() 16 | } 17 | 18 | const deleteUserData = async () => { 19 | if ( 20 | !(await swal({ 21 | title: "Are you sure?", 22 | text: "Are you sure that you want to delete this user?", 23 | icon: "warning", 24 | buttons: ["No, I am not sure!", "Yes, I am sure!"], 25 | })) 26 | ) 27 | return 28 | 29 | await deleteUser(user.userid) 30 | fetchTelegramUsers() 31 | } 32 | 33 | return ( 34 | 41 | {user.userid} 42 | {user.username} 43 | {user.time} 44 | 49 | 62 | 73 | 74 | 75 | ) 76 | } 77 | 78 | export default TableRow 79 | -------------------------------------------------------------------------------- /components/DesktopMenu/DesktopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Link from "next/link" 3 | import dynamic from "next/dynamic" 4 | import { useAdminProvider } from "providers/AdminProvider" 5 | import Media from "../../shared/Media" 6 | import Switch from "../../shared/Switch" 7 | import { useTheme } from "../../providers/ThemeProvider" 8 | 9 | const GoogleLoginButton = dynamic(() => import("../GoogleLoginButton"), { 10 | ssr: false, 11 | }) 12 | 13 | const DesktopMenu = () => { 14 | const { bot } = useAdminProvider() 15 | 16 | const { onChangeThemeConfig, themeMode } = useTheme() 17 | 18 | const onToggle = () => { 19 | onChangeThemeConfig() 20 | } 21 | 22 | return ( 23 |
30 |
36 | 37 |
38 | 47 |

52 | Weather Bot 53 |

54 |
55 | 56 |
57 | 65 |
66 | 67 |
68 |

73 | Light 74 |

75 | 76 |

81 | Dark 82 |

83 |
84 |
85 |
86 | ) 87 | } 88 | 89 | export default DesktopMenu 90 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/Pagination/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { useAdminProvider } from "providers/AdminProvider" 2 | import { 3 | ChevronDoubleLeftIcon, 4 | ChevronDoubleRightIcon, 5 | ChevronLeftIcon, 6 | ChevronRightIcon, 7 | } from "@heroicons/react/24/solid" 8 | import PageButton from "../PageButton" 9 | 10 | const Pagination = () => { 11 | const { 12 | pageIndex, 13 | canPreviousPage, 14 | canNextPage, 15 | pageSize, 16 | setPageSize, 17 | nextPage, 18 | gotoPage, 19 | previousPage, 20 | pageCount, 21 | } = useAdminProvider() 22 | 23 | return ( 24 |
28 |

Page {pageIndex + 1}

29 |
33 |

Items Per Page

34 | 54 |
55 |
56 | 87 |
88 |
89 | ) 90 | } 91 | 92 | export default Pagination 93 | -------------------------------------------------------------------------------- /components/Pages/AdminPage/BotSetting/BotForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import Form from "../../../../shared/Form" 3 | import { inputValidation } from "./inputValidation" 4 | import Button from "../../../../shared/Button" 5 | import { useAdminProvider } from "../../../../providers/AdminProvider" 6 | import Input from "../../../../shared/Input" 7 | import { restartBot } from "../../../../lib/weatherBot" 8 | 9 | const BotForm = () => { 10 | const [newBotKey, setNewBotKey] = useState("") 11 | const [newHandle, setNewHandle] = useState("") 12 | 13 | const { bot, fetchBotData } = useAdminProvider() 14 | 15 | const launch = async (values: any) => { 16 | await restartBot(values.newKey, bot.key, values.newHandle) 17 | 18 | await fetchBotData() 19 | } 20 | 21 | return ( 22 |
{ 24 | launch(values) 25 | }} 26 | validationSchema={inputValidation} 27 | className="w-full flex flex-col gap-y-[10px]" 28 | > 29 |
30 |

34 | Handle: 35 |

36 | setNewHandle(e.target.value)} 39 | className="placeholder:text-[gray] 40 | rounded-[5px] 41 | font-poppins 42 | text-[10px] md:text-[16px] 43 | md:w-[300px] md:h-[45px] 44 | w-[180px] h-[30px]" 45 | placeholder={`${bot?.handle || "Ex. WeatherHenryBot"}`} 46 | id="newHandle" 47 | name="newHandle" 48 | hookToForm 49 | /> 50 |
51 |
52 |

56 | BotKey: 57 |

58 | setNewBotKey(e.target.value)} 61 | className="placeholder:text-[gray] 62 | rounded-[5px] 63 | font-poppins 64 | text-[10px] md:text-[16px] 65 | md:w-[500px] md:h-[45px] 66 | w-[180px] h-[30px]" 67 | placeholder={`${bot?.key || "Ex. 6576938381:AAG...VVK_NM"}`} 68 | id="newKey" 69 | name="newKey" 70 | hookToForm 71 | /> 72 | 84 |
85 |
86 | ) 87 | } 88 | 89 | export default BotForm 90 | -------------------------------------------------------------------------------- /providers/AdminProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext, useCallback, useMemo, useEffect, useState } from "react" 2 | import getTelegramUsers from "../lib/getTelegramUsers" 3 | import { getBotKey } from "../lib/weatherBot" 4 | 5 | const AdminContext = createContext(null) 6 | 7 | export const AdminProvider = ({ children }: any) => { 8 | const [bot, setBot] = useState(null) 9 | const [telegramUsers, setTelegramUsers] = useState(null) 10 | const [filterKey, setFilterKey] = useState("") 11 | const [filterUsers, setFilterUsers] = useState(null) 12 | const [paginatedUser, setPaginatedUsers] = useState(null) 13 | 14 | const [pageCount, setPageCount] = useState(0) 15 | const [pageSize, setPageSize] = useState(5) 16 | const [pageIndex, setPageIndex] = useState(0) 17 | 18 | const canPreviousPage = useMemo(() => { 19 | return pageIndex 20 | }, [pageIndex]) 21 | 22 | const canNextPage = useMemo(() => { 23 | return pageIndex < pageCount - 1 24 | }, [pageIndex, pageCount]) 25 | 26 | const gotoPage = (index: number) => setPageIndex(index) 27 | const nextPage = () => { 28 | if (canNextPage) setPageIndex(pageIndex + 1) 29 | } 30 | const previousPage = () => { 31 | if (canPreviousPage) setPageIndex(pageIndex - 1) 32 | } 33 | 34 | const fetchBotData = useCallback(async () => { 35 | const response: any = await getBotKey() 36 | 37 | if (!response.err) setBot(response) 38 | }, []) 39 | 40 | const fetchTelegramUsers = useCallback(async () => { 41 | try { 42 | const response = await getTelegramUsers() 43 | setTelegramUsers(response) 44 | setFilterUsers(response) 45 | } catch (err) {} 46 | }, []) 47 | 48 | useEffect(() => { 49 | fetchBotData() 50 | }, [fetchBotData]) 51 | 52 | useEffect(() => { 53 | if (filterUsers) { 54 | if (!canNextPage) { 55 | setPaginatedUsers(filterUsers.slice(pageIndex * pageSize, filterUsers.length)) 56 | return 57 | } 58 | 59 | setPaginatedUsers(filterUsers.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)) 60 | } 61 | }, [canNextPage, pageSize]) 62 | 63 | useEffect(() => { 64 | if (filterUsers) { 65 | setPageCount(parseInt(Number(filterUsers.length / pageSize + 1).toFixed(0))) 66 | setPaginatedUsers(filterUsers.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)) 67 | } 68 | }, [filterUsers]) 69 | 70 | useEffect(() => { 71 | setPageIndex(0) 72 | }, [pageSize]) 73 | 74 | useEffect(() => { 75 | if (telegramUsers) { 76 | const filterTemp = telegramUsers?.filter( 77 | (user: any) => user.username.toLowerCase().search(filterKey.toLowerCase()) >= 0, 78 | ) 79 | setFilterUsers(filterTemp) 80 | } 81 | }, [filterKey]) 82 | 83 | useEffect(() => { 84 | fetchTelegramUsers() 85 | }, [fetchTelegramUsers]) 86 | 87 | const provider = useMemo( 88 | () => ({ 89 | canPreviousPage, 90 | canNextPage, 91 | filterUsers, 92 | fetchTelegramUsers, 93 | filterKey, 94 | setFilterKey, 95 | pageSize, 96 | setPageSize, 97 | pageCount, 98 | pageIndex, 99 | gotoPage, 100 | paginatedUser, 101 | nextPage, 102 | previousPage, 103 | bot, 104 | fetchBotData, 105 | }), 106 | [ 107 | canPreviousPage, 108 | canNextPage, 109 | fetchTelegramUsers, 110 | filterKey, 111 | setFilterKey, 112 | filterUsers, 113 | pageSize, 114 | setPageSize, 115 | pageCount, 116 | pageIndex, 117 | gotoPage, 118 | paginatedUser, 119 | nextPage, 120 | previousPage, 121 | bot, 122 | fetchBotData, 123 | ], 124 | ) 125 | 126 | return {children} 127 | } 128 | 129 | export const useAdminProvider = () => useContext(AdminContext) 130 | -------------------------------------------------------------------------------- /public/fonts/Poppins/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | --------------------------------------------------------------------------------