initial framework
This commit is contained in:
25
README.md
25
README.md
@@ -1,8 +1,8 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
## LMGCITFY
|
||||||
|
|
||||||
## Getting Started
|
Useful for people who can't be fucked to just search for it on [GunCAD Index](https://guncadindex.com).
|
||||||
|
|
||||||
First, run the development server:
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
@@ -14,21 +14,4 @@ pnpm dev
|
|||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Development server opens on [localhost:5000](http://localhost:5000)
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
|
|||||||
31
actions.ts
Normal file
31
actions.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { APIResult, GCIAPIResult } from "./types";
|
||||||
|
|
||||||
|
const feelingFree = async (query: string): Promise<APIResult> => {
|
||||||
|
console.log(query);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://guncadindex.com/api/releases/?${query}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(response.status);
|
||||||
|
return { error: true, payload: "Something went wrong." };
|
||||||
|
}
|
||||||
|
const { results }: GCIAPIResult = await response.json();
|
||||||
|
const randomResult = results[Math.round(Math.random() * results.length)];
|
||||||
|
const slug = randomResult.url.split("/").filter(Boolean).pop();
|
||||||
|
if (!slug) {
|
||||||
|
return { error: true, payload: "Could not retrieve results." };
|
||||||
|
}
|
||||||
|
console.log(slug);
|
||||||
|
return { payload: `https://guncadindex.com/detail/${slug}` };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return { error: true, payload: error.message };
|
||||||
|
}
|
||||||
|
return { error: true, payload: "Something went wrong." };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { feelingFree };
|
||||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1,9 +1,25 @@
|
|||||||
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { IBM_Plex_Sans, IBM_Plex_Mono } from 'next/font/google';
|
||||||
|
import { Provider } from "@/components/ui/provider";
|
||||||
|
import { Flex } from "@chakra-ui/react";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
const ibmPlexSans = IBM_Plex_Sans({
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-ibm'
|
||||||
|
});
|
||||||
|
|
||||||
|
const ibmPlexMono = IBM_Plex_Mono({
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: '600',
|
||||||
|
variable: '--font-ibm-mono'
|
||||||
|
})
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "LMGCITFY",
|
||||||
description: "Generated by create next app",
|
description: "Let me GCI that for you.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -12,8 +28,15 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning={true}>
|
||||||
<body>{children}</body>
|
<body className={`${ibmPlexSans.variable} ${ibmPlexMono.variable}`}>
|
||||||
|
<Provider>
|
||||||
|
<Flex backgroundImage={'url(/background.gif)'} backgroundRepeat={'repeat'} minH={'100vh'} minW={'full'} justify={'center'} align={'center'}>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
<Toaster />
|
||||||
|
</Provider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
app/page.tsx
68
app/page.tsx
@@ -1,7 +1,69 @@
|
|||||||
|
'use client'
|
||||||
|
import theme from "@/theme";
|
||||||
|
import { Button, ChakraProvider, Container, Flex, Heading, Image, Input, Text } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toaster } from "@/components/ui/toaster";
|
||||||
|
import { feelingFree } from "@/actions";
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [query, setQuery] = useState<string | undefined>(undefined);
|
||||||
|
const feelingFreedom = async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
throw new Error('You must provide a search query.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.set('query', query)
|
||||||
|
|
||||||
|
const result = await feelingFree(searchParams.toString());
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
toaster.create({ title: 'Redirecting...', description: 'Was that so hard?', type: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = result.payload;
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toaster.create({ type: 'error', title: 'Error', description: error.message })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const lmgcitfy = () => {
|
||||||
|
if (!query) {
|
||||||
|
toaster.create({ title: 'Error', description: 'You must enter a search query.', type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
searchParams.append('query', query);
|
||||||
|
window.location.href = `https://guncadindex.com/search?${searchParams}`;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<main>
|
<ChakraProvider value={theme}>
|
||||||
<div>Hello world!</div>
|
<Container backgroundColor={'#2a2a2a'} borderRadius={'lg'} boxShadow={'0 0 12px #4a4a4a'} py={4} width={{ base: '90%', md: '50%' }} overflow={'hidden'}>
|
||||||
</main>
|
<Flex justify={'center'} align={'center'} p={2} mb={4}>
|
||||||
|
<Image src={'/gci_logo_large.png'} height={{ base: '50px', md: '75px' }} alt="GunCAD Index logo" />
|
||||||
|
<Flex flexDir={'column'} mx={4} >
|
||||||
|
<Heading size={{ base: '2xl', md: '5xl' }} fontFamily={'var(--font-ibm)'} lineHeight={1}>GunCAD Index</Heading>
|
||||||
|
<Text fontSize={{ base: 'sm', md: 'xl' }} ml={{ md: 1 }} fontFamily={'var(--font-ibm)'}>A search engine for guns.</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Input variant={'outline'} borderRadius={'lg'} border={'1px solid #4a4a4a'} size={'lg'} placeholder="Enter your query here" onChange={(e) => setQuery(e.target.value)} />
|
||||||
|
<Flex justify={'space-around'} gapY={4} flexDir={{ base: 'column', md: 'row' }} p={2} my={4}>
|
||||||
|
<Button colorPalette={'green'} variant={'solid'} size={'lg'} onClick={() => lmgcitfy()}>Search on GCI</Button>
|
||||||
|
<Button colorPalette={'white'} variant={'outline'} size={'lg'} border={'1px solid #4a4a4a'} color={'#FFF'} onClick={() => feelingFreedom()}>I'm feeling free</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
108
components/ui/color-mode.tsx
Normal file
108
components/ui/color-mode.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
||||||
|
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
||||||
|
import { ThemeProvider, useTheme } from "next-themes"
|
||||||
|
import type { ThemeProviderProps } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuMoon, LuSun } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||||
|
|
||||||
|
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorMode = "light" | "dark"
|
||||||
|
|
||||||
|
export interface UseColorModeReturn {
|
||||||
|
colorMode: ColorMode
|
||||||
|
setColorMode: (colorMode: ColorMode) => void
|
||||||
|
toggleColorMode: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorMode(): UseColorModeReturn {
|
||||||
|
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
||||||
|
const colorMode = forcedTheme || resolvedTheme
|
||||||
|
const toggleColorMode = () => {
|
||||||
|
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
colorMode: colorMode as ColorMode,
|
||||||
|
setColorMode: setTheme,
|
||||||
|
toggleColorMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorModeValue<T>(light: T, dark: T) {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "dark" ? dark : light
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColorModeIcon() {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||||
|
|
||||||
|
export const ColorModeButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ColorModeButtonProps
|
||||||
|
>(function ColorModeButton(props, ref) {
|
||||||
|
const { toggleColorMode } = useColorMode()
|
||||||
|
return (
|
||||||
|
<ClientOnly fallback={<Skeleton boxSize="9" />}>
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Toggle color mode"
|
||||||
|
size="sm"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
css={{
|
||||||
|
_icon: {
|
||||||
|
width: "5",
|
||||||
|
height: "5",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorModeIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ClientOnly>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function LightMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color="fg"
|
||||||
|
display="contents"
|
||||||
|
className="chakra-theme light"
|
||||||
|
colorPalette="gray"
|
||||||
|
colorScheme="light"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function DarkMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color="fg"
|
||||||
|
display="contents"
|
||||||
|
className="chakra-theme dark"
|
||||||
|
colorPalette="gray"
|
||||||
|
colorScheme="dark"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
15
components/ui/provider.tsx
Normal file
15
components/ui/provider.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
ColorModeProvider,
|
||||||
|
type ColorModeProviderProps,
|
||||||
|
} from "./color-mode"
|
||||||
|
|
||||||
|
export function Provider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ChakraProvider value={defaultSystem}>
|
||||||
|
<ColorModeProvider {...props} />
|
||||||
|
</ChakraProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
components/ui/toaster.tsx
Normal file
43
components/ui/toaster.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toaster as ChakraToaster,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Toast,
|
||||||
|
createToaster,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export const toaster = createToaster({
|
||||||
|
placement: "bottom-end",
|
||||||
|
pauseOnPageIdle: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Toaster = () => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||||
|
{(toast) => (
|
||||||
|
<Toast.Root width={{ md: "sm" }}>
|
||||||
|
{toast.type === "loading" ? (
|
||||||
|
<Spinner size="sm" color="blue.solid" />
|
||||||
|
) : (
|
||||||
|
<Toast.Indicator />
|
||||||
|
)}
|
||||||
|
<Stack gap="1" flex="1" maxWidth="100%">
|
||||||
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||||
|
{toast.description && (
|
||||||
|
<Toast.Description>{toast.description}</Toast.Description>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{toast.action && (
|
||||||
|
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||||
|
)}
|
||||||
|
{toast.closable && <Toast.CloseTrigger />}
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</ChakraToaster>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
components/ui/tooltip.tsx
Normal file
46
components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||||
|
showArrow?: boolean
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement | null>
|
||||||
|
content: React.ReactNode
|
||||||
|
contentProps?: ChakraTooltip.ContentProps
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
|
function Tooltip(props, ref) {
|
||||||
|
const {
|
||||||
|
showArrow,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
portalled = true,
|
||||||
|
content,
|
||||||
|
contentProps,
|
||||||
|
portalRef,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (disabled) return children
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraTooltip.Root {...rest}>
|
||||||
|
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraTooltip.Positioner>
|
||||||
|
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||||
|
{showArrow && (
|
||||||
|
<ChakraTooltip.Arrow>
|
||||||
|
<ChakraTooltip.ArrowTip />
|
||||||
|
</ChakraTooltip.Arrow>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</ChakraTooltip.Content>
|
||||||
|
</ChakraTooltip.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</ChakraTooltip.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -2,6 +2,9 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: ["@chakra-ui/react"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
1399
package-lock.json
generated
1399
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "lmgcitfy",
|
"name": "lmgcitfy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"description": "Helping people use the search feature on GunCAD Index",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev -p 5000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^3.32.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"react-icons": "^5.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
BIN
public/background.gif
Normal file
BIN
public/background.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
BIN
public/gci_logo_large.png
Normal file
BIN
public/gci_logo_large.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
17
theme.ts
Normal file
17
theme.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
defaultConfig,
|
||||||
|
createSystem,
|
||||||
|
defineConfig,
|
||||||
|
defineTextStyles,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
const config = defineConfig({
|
||||||
|
globalCss: {
|
||||||
|
"*": {
|
||||||
|
focusRing: "none",
|
||||||
|
color: "#FFF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createSystem(defaultConfig, config);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "esnext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
20
types.d.ts
vendored
Normal file
20
types.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
interface GCIAPIResult {
|
||||||
|
count: number;
|
||||||
|
next: null | number;
|
||||||
|
previous: null | number;
|
||||||
|
results: GCIResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GCIResult {
|
||||||
|
id: string;
|
||||||
|
shortlink: string | null;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APIResult {
|
||||||
|
error?: boolean;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GCIAPIResult, APIResult };
|
||||||
Reference in New Issue
Block a user